├── .gitignore ├── .travis.yml ├── README.md ├── appveyor.yml ├── index.js ├── package.json ├── pound ├── lf.js ├── many-lock-unlock.js ├── t.js └── t.sh └── test ├── basic.js └── fixtures ├── abandon-socket.js ├── exit-no-release.js ├── graceful-exit.js └── server-disconnect.js /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | node_modules 3 | pound/output.txt 4 | pound/slocket-testing* 5 | pound/*.lock 6 | coverage/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4' 5 | - '6' 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # slocket 2 | 3 | A locking socket alternative to file-system mutex locks 4 | 5 | ## Algorithm 6 | 7 | ``` 8 | to ACQUIRE(lockname) 9 | - create server, listen on lockname 10 | - if enotsock, WATCH(lockname) 11 | - if eaddrinuse, 12 | - CONNECT(lockname) 13 | - unref server 14 | - lock has been acquired via server 15 | ! on connection, place sockets in queue 16 | 17 | to RELEASE(lockname) 18 | - if acquired via connection 19 | - if connected, disconnect 20 | - else, unlink lockname 21 | - if acquired via server 22 | - send "OK" to front-most connection 23 | - when connection disconnects, RELEASE(lockname) 24 | - if acquired via filename 25 | - unlink file 26 | 27 | to CONNECT(lockname) 28 | - net.connect(lockname) 29 | - if econnrefused (socket, but not listening!) 30 | could be that a server process crashed, leaving a dangling 31 | connection that thinks it has the lock. 32 | - WATCH(lockname) 33 | - if enoent or on socket termination, ACQUIRE(lockname) 34 | - when server says "OK", 35 | - lock has been acquired via connection 36 | - on connection disconnect, on release, unlink socket 37 | 38 | to WATCH(lockname) 39 | - fs.watch(lockname) 40 | - on change, ACQUIRE(lockname) 41 | ``` 42 | 43 | ## USAGE 44 | 45 | ```js 46 | var slocket = require('slocket') 47 | 48 | // Only one of these can run in this filesystem space, 49 | // even if there are many processes running at once 50 | function someMutexedThing (cb) { 51 | slocket('/path/to/my-lock-name', function (er, lock) { 52 | if (er) throw er 53 | // lock acquired 54 | // do your thing here 55 | // and then... 56 | lock.release() 57 | }) 58 | } 59 | ``` 60 | 61 | A slocket is like a Promise, so this works: 62 | 63 | ```js 64 | slocket('/path/to/filename.lock').then(lock => { 65 | // do your stuff in this space 66 | lock.release() 67 | }).catch(er => { 68 | // a lock could not be acquired 69 | }) 70 | ``` 71 | 72 | If you want to use async/await, you can do this, which is nice: 73 | 74 | ```js 75 | async function fooSingleFile (args) { 76 | var lock = await slocket('foo') 77 | 78 | // now I have an exclusive lock on the fooness! 79 | 80 | await otherAsyncThingie(args) 81 | 82 | // all done, release the mutex 83 | lock.release() 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '6' 4 | - nodejs_version: '4' 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - set CI=true 8 | - npm -g install npm@latest 9 | - set PATH=%APPDATA%\npm;%PATH% 10 | - npm install 11 | matrix: 12 | fast_finish: true 13 | build: off 14 | version: '{build}' 15 | shallow_clone: true 16 | clone_depth: 1 17 | test_script: 18 | - npm test 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = Slocket 2 | 3 | var rimraf = require('rimraf') 4 | var assert = require('assert') 5 | var EE = require('events') 6 | var net = require('net') 7 | var fs = require('fs') 8 | var path = require('path') 9 | var onExit = require('signal-exit') 10 | var locks = Object.create(null) 11 | var ID = 0 12 | 13 | /* istanbul ignore if */ 14 | if (typeof Promise === undefined) 15 | Promise = require('bluebird') 16 | 17 | var util = require('util') 18 | util.inherits(Slocket, EE) 19 | 20 | var debug = function () {} 21 | 22 | /* istanbul ignore if */ 23 | if (/\bslocket\b/i.test(process.env.NODE_DEBUG || '')) { 24 | debug = function () { 25 | var msg = util.format.apply(util, arguments) 26 | var n = path.basename(this.name) 27 | var p = 'SLOCKET:' + process.pid + ':' + n + ':' + this.id + ' ' 28 | msg = p + msg.trimRight().split('\n').join('\n' + p) 29 | console.error(msg) 30 | } 31 | } 32 | 33 | function Slocket (name, cb) { 34 | if (!(this instanceof Slocket)) 35 | return new Slocket(name, cb) 36 | 37 | this.id = ID++ 38 | this.name = path.resolve(name) 39 | /* istanbul ignore next */ 40 | if (process.platform === 'win32' && !/^\\\\.\\pipe\\/i.test(name)) 41 | this.name = '\\\\.\\pipe\\' + name 42 | if (cb) 43 | this.cb = cb 44 | this.server = null 45 | this.connection = null 46 | this.watcher = null 47 | this.has = false 48 | this.had = false 49 | this.connectionQueue = [] 50 | this.currentClient = null 51 | this.windows = process.platform === 'win32' 52 | 53 | this.promise = new Promise(function (resolve, reject) { 54 | this.resolve = resolve 55 | this.reject = reject 56 | }.bind(this)) 57 | this.then = this.promise.then.bind(this.promise) 58 | this.catch = this.promise.catch.bind(this.promise) 59 | this.release = this.release.bind(this) 60 | 61 | this.acquire() 62 | } 63 | 64 | Slocket.prototype.debug = debug 65 | 66 | Slocket.prototype.cb = function () {} 67 | 68 | Slocket.prototype.acquire = function () { 69 | this.debug('acquire') 70 | this.unwatch() 71 | this.disconnect() 72 | this.server = net.createServer(this.onServerConnection.bind(this)) 73 | this.server.once('error', this.onServerError.bind(this)) 74 | this.server.listen(this.name, this.onServerListen.bind(this)) 75 | this.server.on('close', this.onServerClose.bind(this)) 76 | this.server.unref() 77 | } 78 | 79 | Slocket.prototype.onAcquire = function () { 80 | this.debug('onAcquire') 81 | this.unwatch() 82 | assert.equal(this.has, false) 83 | this.has = true 84 | this.had = true 85 | this.emit('acquire') 86 | this.cb(null, this) 87 | // Promises are sometimes a little clever 88 | // when you resolve(), it hooks onto the .then method 89 | // of the promise it's resolving to. To avoid never actually 90 | // resolving, we wrap to hide the then/catch methods. 91 | this.resolve(Object.create(this, { 92 | then: { value: undefined }, 93 | catch: { value: undefined }, 94 | resolve: { value: undefined }, 95 | reject: { value: undefined }, 96 | release: { value: this.release.bind(this) } 97 | })) 98 | } 99 | 100 | Slocket.prototype.onServerListen = function () { 101 | this.server.listening = true 102 | this.debug('onServerListen', this.server.listening) 103 | this.emit('serverListen') 104 | process.nextTick(function onServerListenNT () { 105 | this.debug('onServerListenNT', this.server.listening) 106 | this.on('serverClose', onExit(this.onProcessExit.bind(this))) 107 | this.onAcquire() 108 | }.bind(this)) 109 | } 110 | 111 | Slocket.prototype.onProcessExit = function () { 112 | this.debug('onProcessExit') 113 | if (this.has === true) 114 | this.release(true) 115 | } 116 | 117 | Slocket.prototype.onServerConnection = function (c) { 118 | this.debug('onServerConnection') 119 | this.emit('serverConnection', c) 120 | c.on('close', this.onServerConnectionClose.bind(this, c)) 121 | 122 | // Nearly impossible, but the race condition between the server 123 | // closing after releasing a lock, and a client connecting can 124 | // sometimes be hit in the pound test. Ignore for test coverage. 125 | /* istanbul ignore else */ 126 | if (this.currentClient || this.has) 127 | this.connectionQueue.push(c) 128 | else 129 | this.delegate(c) 130 | } 131 | 132 | Slocket.prototype.onServerConnectionClose = function (c) { 133 | this.debug('onServerConnectionClose', this.has) 134 | this.emit('serverConnectionClose', c) 135 | if (this.currentClient === c) { 136 | this.currentClient = null 137 | this.release() 138 | } 139 | 140 | var i = this.connectionQueue.indexOf(c) 141 | if (i !== -1) 142 | this.connectionQueue.splice(i, 1) 143 | } 144 | 145 | Slocket.prototype.delegate = function (c) { 146 | if (this.windows) 147 | return this.serverClose(c) 148 | 149 | this.debug('delegate') 150 | assert.equal(this.has, false) 151 | this.debug('delegate new client') 152 | this.currentClient = c 153 | c.write('OK') 154 | } 155 | 156 | Slocket.prototype.type = function () { 157 | return !this.has ? 'none' 158 | : this.server && this.server.listening ? 'server' 159 | : this.connection ? 'connection' 160 | : 'wtf' 161 | } 162 | 163 | Slocket.prototype.release = function (sync) { 164 | this.debug('release has=%j sync=%j', this.has, sync) 165 | this.has = false 166 | this.connectionRelease(sync) 167 | this.serverRelease(sync) 168 | } 169 | 170 | Slocket.prototype.serverRelease = function (sync) { 171 | if (!this.server) 172 | return 173 | 174 | this.debug('serverRelease %j', sync, this.connectionQueue.length) 175 | if (this.connectionQueue.length) 176 | this.delegate(this.connectionQueue.shift()) 177 | else 178 | this.serverClose() 179 | } 180 | 181 | Slocket.prototype.serverClose = function (conn) { 182 | this.debug('serverClose', this.connectionQueue.length) 183 | this.server.listening = false 184 | this.server.close() 185 | if (conn) 186 | conn.destroy() 187 | this.connectionQueue.forEach(function (connection) { 188 | connection.destroy() 189 | }) 190 | this.connectionQueue.length = 0 191 | } 192 | 193 | Slocket.prototype.onServerClose = function () { 194 | this.debug('onServerClose') 195 | this.emit('serverClose') 196 | this.server = null 197 | } 198 | 199 | Slocket.prototype.onServerError = function (er) { 200 | this.debug('onServerError', er.message) 201 | this.emit('serverError', er) 202 | this.server = null 203 | switch (er.code) { 204 | case 'ENOTSOCK': 205 | return this.watch() 206 | case 'EADDRINUSE': 207 | case 'EEXIST': 208 | return this.connect() 209 | default: 210 | er.slocket = 'server' 211 | this.onError(er) 212 | } 213 | } 214 | 215 | Slocket.prototype.onError = function (er) { 216 | this.debug('onError', er.message) 217 | this.cb(er, this) 218 | this.reject(er) 219 | } 220 | 221 | Slocket.prototype.connect = function () { 222 | this.debug('connect') 223 | this.connection = net.createConnection(this.name) 224 | this.connection.slocketBuffer = '' 225 | this.connection.setEncoding('utf8') 226 | this.connection.slocketConnected = false 227 | this.connection.on('connect', this.onConnect.bind(this)) 228 | this.connection.on('error', this.onConnectionError.bind(this)) 229 | this.connection.on('data', this.onConnectionData.bind(this)) 230 | } 231 | 232 | Slocket.prototype.onConnectionData = function (chunk) { 233 | this.debug('onConnectionData %s', chunk, this.connection.slocketBuffer) 234 | this.emit('connectionData', chunk) 235 | this.connection.slocketBuffer += chunk 236 | 237 | if (this.connection.slocketBuffer === 'OK') 238 | this.onAcquire() 239 | 240 | if (this.connection.slocketBuffer.length > 2) 241 | this.connection.destroy() 242 | } 243 | 244 | Slocket.prototype.onConnectionError = function (er) { 245 | this.debug('onConnectionError', er.message) 246 | this.emit('connectionError', er) 247 | if (this.has) 248 | return this.onError(er) 249 | this.connection = null 250 | switch (er.code) { 251 | case 'ENOENT': 252 | // socket was there, but now is gone! 253 | return this.acquire() 254 | 255 | case 'ENOTSOCK': 256 | // something other than a socket is there already 257 | case 'ECONNREFUSED': 258 | // socket there, but not listening 259 | // watch for changes, in case it's that it's not a socket 260 | // if that fails, eg for "unknown system error", then just retry 261 | try { 262 | return this.watch() 263 | } catch (er) { 264 | return this.acquire() 265 | } 266 | default: 267 | er.slocket = 'connection' 268 | this.onError(er) 269 | } 270 | } 271 | 272 | Slocket.prototype.onConnect = function () { 273 | this.debug('onConnect') 274 | this.emit('connect') 275 | this.connection.slocketConnected = true 276 | this.connection.on('close', this.onConnectionClose.bind(this)) 277 | } 278 | 279 | Slocket.prototype.onConnectionClose = function () { 280 | this.debug('onConnectionClose') 281 | this.emit('connectionClose') 282 | this.connection.slocketConnected = false 283 | if (!this.had) 284 | this.acquire() 285 | } 286 | 287 | Slocket.prototype.connectionRelease = function (sync) { 288 | if (!this.connection) 289 | return 290 | 291 | this.debug('connectionRelease', sync) 292 | 293 | if (this.connection.slocketConnected) 294 | this.connection.destroy() 295 | else if (sync) 296 | rimraf.sync(this.name) 297 | else 298 | rimraf(this.name, function () {}) 299 | } 300 | 301 | Slocket.prototype.disconnect = function () { 302 | this.debug('disconnect') 303 | if (this.connection) 304 | this.connection.destroy() 305 | this.connection = null 306 | } 307 | 308 | Slocket.prototype.watch = function () { 309 | this.debug('watch') 310 | this.watcher = fs.watch(this.name, { persistent: false }) 311 | this.watcher.on('change', this.acquire.bind(this)) 312 | this.watch.on('error', this.acquire.bind(this)) 313 | } 314 | 315 | Slocket.prototype.unwatch = function () { 316 | this.debug('unwatch') 317 | if (this.watcher) 318 | this.watcher.close() 319 | this.watcher = null 320 | } 321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slocket", 3 | "version": "1.0.5", 4 | "description": "A locking socket alternative to file-system mutex locks", 5 | "main": "index.js", 6 | "dependencies": { 7 | "rimraf": "^2.5.4", 8 | "bluebird": "^3.4.7", 9 | "signal-exit": "^3.0.2" 10 | }, 11 | "devDependencies": { 12 | "lockfile": "^1.0.3", 13 | "tap": "^10.0.0" 14 | }, 15 | "scripts": { 16 | "test": "tap test/*.js --cov", 17 | "preversion": "npm test", 18 | "postversion": "npm publish", 19 | "postpublish": "git push origin --all; git push origin --tags" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/isaacs/slocket" 24 | }, 25 | "keywords": [ 26 | "mutex", 27 | "lock", 28 | "socket", 29 | "file system", 30 | "lockfile" 31 | ], 32 | "author": "Isaac Z. Schlueter (http://blog.izs.me/)", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/isaacs/slocket/issues" 36 | }, 37 | "homepage": "https://github.com/isaacs/slocket#readme", 38 | "files": [ 39 | "index.js" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /pound/lf.js: -------------------------------------------------------------------------------- 1 | var lockfile = require('lockfile') 2 | var MS = +process.argv[2] || 50 3 | var N = +process.argv[3] || 5 4 | var IMMEDIATE = MS === 0 5 | var PARALLEL = process.argv[5] === 'parallel' 6 | var res = { pid: process.pid, start: Date.now() } 7 | var onExit = require('signal-exit') 8 | var gotAll = false 9 | var got = 0 10 | 11 | runTests() 12 | 13 | // Only one of these can run in this filesystem space, 14 | // even if there are many processes running at once 15 | function runTests () { 16 | if (PARALLEL) 17 | parallelTests() 18 | else 19 | serialTests() 20 | } 21 | 22 | function parallelTests () { 23 | for (var i = 0; i < N; i++) 24 | runTest(i) 25 | } 26 | 27 | function serialTests (i) { 28 | i = i || 0 29 | if (i < N) 30 | runTest(i, serialTests.bind(null, i + 1)) 31 | } 32 | 33 | function runTest (i, cb) { 34 | var jitter = 0 // Math.floor((Math.random() - .5)*(MS * 0.1)) 35 | res[i] = { 36 | start : Date.now(), 37 | jitter: jitter 38 | } 39 | var name = __dirname + '/slocket-testing-'+i 40 | lockfile.lock(name, { 41 | retries: Infinity, 42 | retryWait: 0 43 | }, function (er, lock) { 44 | if (er) throw er 45 | res[i].acquired = Date.now() 46 | if (IMMEDIATE) 47 | done() 48 | else 49 | setTimeout(done, MS + jitter) 50 | 51 | function done () { 52 | if (++got === N) 53 | gotAll = true 54 | res[i].release = Date.now() 55 | res[i].holdDuration = res[i].release - res[i].acquired 56 | res[i].totalDur = res[i].release - res[i].start 57 | lockfile.unlock(name) 58 | if (cb) cb() 59 | } 60 | }) 61 | } 62 | 63 | onExit(function (code, signal) { 64 | res.code = code 65 | res.signal = signal 66 | res.gotAll = gotAll 67 | res.got = got 68 | res.end = Date.now() 69 | res.dur = res.end - res.start 70 | console.log('%j', res) 71 | }) 72 | -------------------------------------------------------------------------------- /pound/many-lock-unlock.js: -------------------------------------------------------------------------------- 1 | var slocket = require('../') 2 | var filename = __dirname + '/many-lock-unlock.lock' 3 | var N = 1000 4 | var lockfile = require('lockfile') 5 | var rimraf = require('rimraf') 6 | 7 | function parallel (cb) { 8 | rimraf.sync(filename) 9 | var start = Date.now() 10 | var did = 0 11 | for (var i = 0; i < N; i++) { 12 | slocket(filename, function (er) { 13 | if (er) 14 | throw er 15 | this.release() 16 | if (++did === N) { 17 | var dur = Date.now() - start 18 | console.log('parallel %d/%dms => %d q/s, %d ms/q', N, dur, 19 | Math.round(N/dur * 1000), 20 | Math.round(dur/N*1000)/1000) 21 | if (cb) cb() 22 | } 23 | }) 24 | } 25 | } 26 | 27 | function serial (cb, i, start) { 28 | if (!i && !start) { 29 | rimraf.sync(filename) 30 | i = i || 0 31 | start = start || Date.now() 32 | } 33 | if (i === N) { 34 | var dur = Date.now() - start 35 | console.log('serial %d/%dms => %d q/s, %d ms/q', N, dur, 36 | Math.round(N/dur * 1000), 37 | Math.round(dur/N * 1000)/1000) 38 | if (cb) cb() 39 | return 40 | } 41 | slocket(filename, function (er) { 42 | if (er) 43 | throw er 44 | this.release() 45 | serial(cb, i + 1, start) 46 | }) 47 | } 48 | 49 | function lfp (cb) { 50 | rimraf.sync(filename) 51 | var start = Date.now() 52 | var did = 0 53 | for (var i = 0; i < N; i++) { 54 | lockfile.lock(filename, { retries: Infinity }, function (er) { 55 | if (er) 56 | throw er 57 | lockfile.unlock(filename, function () { 58 | if (++did === N) { 59 | var dur = Date.now() - start 60 | console.log('lf parallel %d/%dms => %d q/s, %d ms/q', N, dur, 61 | Math.round(N/dur * 1000), 62 | Math.round(dur/N*1000)/1000) 63 | if (cb) cb() 64 | } 65 | }) 66 | }) 67 | } 68 | } 69 | 70 | function lfs (cb, i, start) { 71 | if (!i && !start) { 72 | rimraf.sync(filename) 73 | i = i || 0 74 | start = start || Date.now() 75 | } 76 | if (i === N) { 77 | var dur = Date.now() - start 78 | console.log('lfs %d/%dms => %d q/s, %d ms/q', N, dur, 79 | Math.round(N/dur * 1000), 80 | Math.round(dur/N * 1000)/1000) 81 | if (cb) cb() 82 | return 83 | } 84 | lockfile.lock(filename, function (er) { 85 | if (er) 86 | throw er 87 | lockfile.unlock(filename, function () { 88 | lfs(cb, i + 1, start) 89 | }) 90 | }) 91 | } 92 | 93 | parallel(() => serial(() => lfp(lfs))) 94 | -------------------------------------------------------------------------------- /pound/t.js: -------------------------------------------------------------------------------- 1 | var slocket = require('../') 2 | var MS = +process.argv[2] || 50 3 | var N = +process.argv[3] || 5 4 | var PARALLEL = process.argv[4] === 'parallel' 5 | var IMMEDIATE = MS === 0 6 | var res = { pid: process.pid, start: Date.now() } 7 | var onExit = require('signal-exit') 8 | var gotAll = false 9 | var got = 0 10 | 11 | runTests() 12 | 13 | // Only one of these can run in this filesystem space, 14 | // even if there are many processes running at once 15 | function runTests () { 16 | if (PARALLEL) 17 | parallelTests() 18 | else 19 | serialTests() 20 | } 21 | 22 | function parallelTests () { 23 | for (var i = 0; i < N; i++) 24 | runTest(i) 25 | } 26 | 27 | function serialTests (i) { 28 | i = i || 0 29 | if (i < N) 30 | runTest(i, serialTests.bind(null, i + 1)) 31 | } 32 | 33 | function runTest (i, cb) { 34 | var jitter = 0 // Math.floor((Math.random() - .5)*(MS * 0.1)) 35 | res[i] = { 36 | start : Date.now(), 37 | jitter: jitter 38 | } 39 | slocket(__dirname + '/slocket-testing-'+i, function (er, lock) { 40 | if (er) throw er 41 | res[i].acquired = Date.now() 42 | res[i].has = lock.has 43 | res[i].how = lock.server ? 'server' 44 | : lock.connection ? 'connection' 45 | : lock 46 | if (IMMEDIATE) 47 | done() 48 | else 49 | setTimeout(done, MS + jitter) 50 | 51 | function done () { 52 | if (++got === N) 53 | gotAll = true 54 | res[i].release = Date.now() 55 | res[i].holdDuration = res[i].release - res[i].acquired 56 | res[i].totalDur = res[i].release - res[i].start 57 | lock.release() 58 | if (cb) cb() 59 | } 60 | }) 61 | } 62 | 63 | onExit(function (code, signal) { 64 | res.code = code 65 | res.signal = signal 66 | res.gotAll = gotAll 67 | res.got = got 68 | res.end = Date.now() 69 | res.dur = res.end - res.start 70 | console.log('%j', res) 71 | }) 72 | -------------------------------------------------------------------------------- /pound/t.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | killall -SIGKILL node &>/dev/null 5 | rm -f output.txt slocket-testing* 6 | 7 | N=100 8 | M=10 9 | MS=0 10 | 11 | runParallel () { 12 | rm -f slocket-testing* 13 | echo 'parallel' 14 | echo 'parallel' >&2 15 | let i=0 16 | while [ $i -lt $N ]; do 17 | node t.js $MS $M parallel & 18 | let i++ 19 | done 20 | wait 21 | } 22 | 23 | runSerial () { 24 | rm -f slocket-testing* 25 | echo 'serial' 26 | echo 'serial' >&2 27 | let i=0 28 | while [ $i -lt $N ]; do 29 | node t.js $MS $M & 30 | let i++ 31 | done 32 | wait 33 | } 34 | 35 | lfParallel () { 36 | rm -f slocket-testing* 37 | echo 'lf-parallel' 38 | echo 'lf-parallel' >&2 39 | let i=0 40 | while [ $i -lt $N ]; do 41 | node lf.js $MS $M parallel & 42 | let i++ 43 | done 44 | wait 45 | } 46 | 47 | lfSerial () { 48 | rm -f slocket-testing* 49 | echo 'lf-serial' 50 | echo 'lf-serial' >&2 51 | let i=0 52 | while [ $i -lt $N ]; do 53 | node lf.js $MS $M & 54 | let i++ 55 | done 56 | wait 57 | } 58 | 59 | time runParallel >> output.txt 60 | time runSerial >> output.txt 61 | time lfParallel >> output.txt 62 | time lfSerial >> output.txt 63 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | var Slocket = require('../') 2 | var rimraf = require('rimraf') 3 | var node = process.execPath 4 | var spawn = require('child_process').spawn 5 | var fs = require('fs') 6 | var t = require('tap') 7 | var net = require('net') 8 | 9 | t.jobs = +process.env.JOBS || 4 10 | 11 | var windows = process.platform === 'win32' 12 | 13 | if (typeof Promise === 'undefined') 14 | Promise = require('bluebird') 15 | 16 | t.teardown(function () { 17 | names.forEach(function (n) { 18 | clear(n) 19 | }) 20 | }) 21 | 22 | var names = [] 23 | var lockPrefix = windows ? ('\\\\.\\pipe\\' + __dirname + '\\') 24 | : (__dirname + '/') 25 | 26 | function filename (n) { 27 | names.push(n) 28 | clear(n) 29 | return lockPrefix + n 30 | } 31 | function clear (n) { 32 | try { rimraf.sync(lockPrefix + n) } catch (er) {} 33 | } 34 | 35 | t.test('3 parallel locks', function (t) { 36 | var locks = [] 37 | clear('3-parallel') 38 | var file = filename('3-parallel') 39 | 40 | // also tests returning a promise 41 | for (var i = 0; i < 3; i++) { 42 | locks[i] = Slocket(file) 43 | } 44 | 45 | setInterval(function () { 46 | var h = has() 47 | if (h.filter(h => h).length > 1) 48 | throw new Error('double-lock: ' + JSON.stringify(h)) 49 | }, 1000).unref() 50 | 51 | function has () { 52 | return locks.map(function (l, i) { 53 | return l.has 54 | }) 55 | } 56 | 57 | t.same(has(), [ false, false, false ], 'no locks acquired sync') 58 | 59 | locks.forEach(function (l, i) { 60 | l.then(acquired(i)) 61 | }) 62 | 63 | var got = [] 64 | function acquired (i) { return function (lock) { 65 | t.comment('acquired %d', i) 66 | t.equal(got.indexOf(i), -1, 'should not have gotten already') 67 | got.push(i) 68 | var expect = [false, false, true] 69 | t.same(has().sort(), expect, 'should have exactly 1 lock') 70 | t.isa(lock, Slocket) 71 | t.test('should not look like a promise or deferred', function (t) { 72 | t.equal(lock.then, undefined) 73 | t.equal(lock.catch, undefined) 74 | t.equal(lock.resolve, undefined) 75 | t.equal(lock.reject, undefined) 76 | t.end() 77 | }) 78 | setTimeout(function () { 79 | lock.release() 80 | }, 100) 81 | }} 82 | 83 | Promise.all(locks).then(function (locks) { 84 | setTimeout(function () { 85 | t.same(has(), [false, false, false], 'no remaining locks') 86 | t.same(got.sort(), [ 0, 1, 2 ], 'got all 3 locks') 87 | clear('3-parallel') 88 | t.end() 89 | }, 100) 90 | }) 91 | }) 92 | 93 | t.test('3 serial locks', function (t) { 94 | clear('3-serial') 95 | var file = filename('3-serial') 96 | function go (i) { 97 | if (i === 3) 98 | return t.end() 99 | Slocket(file, function (er, lock) { 100 | if (er) 101 | throw er 102 | t.pass('got lock ' + i) 103 | lock.release() 104 | go(i+1) 105 | }) 106 | } 107 | go(0) 108 | }) 109 | 110 | t.test('staggered', function (t) { 111 | clear('3-staggered') 112 | var file = filename('3-staggered') 113 | 114 | var set = [] 115 | Slocket(file).then(function (lock) { 116 | set[0] = lock 117 | t.equal(lock.type(), 'server') 118 | Slocket(file, function (er, lock) { 119 | t.equal(lock.type(), windows ? 'server' : 'connection') 120 | set[1] = lock 121 | 122 | Slocket(file, function (er, lock) { 123 | t.equal(lock.type(), windows ? 'server' : 'connection') 124 | lock.release() 125 | t.end() 126 | }) 127 | setTimeout(function () { 128 | lock.release() 129 | }, 100) 130 | }).on('connect', function () { 131 | lock.release() 132 | }) 133 | }) 134 | }) 135 | 136 | t.test('server disconnect', { 137 | skip: windows ? 'skip on windows' : false 138 | }, function (t) { 139 | var file = filename('server-disconnect') 140 | var prog = require.resolve('./fixtures/server-disconnect.js') 141 | var child = spawn(node, [prog, file]) 142 | child.stderr.pipe(process.stderr) 143 | child.stdout.on('data', function () { 144 | // now we know that the server has the lock 145 | var didKill = false 146 | setTimeout(function () { 147 | child.kill('SIGHUP') 148 | }) 149 | 150 | var clients = [ 151 | Slocket(file, onLock), 152 | Slocket(file, onLock), 153 | Slocket(file, onLock), 154 | Slocket(file, onLock), 155 | Slocket(file, onLock) 156 | ] 157 | Promise.all(clients).then(t.end) 158 | 159 | function onLock (er, lock) { 160 | var has = clients.filter(function (c) { return c.has }) 161 | t.equal(has.length, 1, 'always exactly one lock') 162 | if (!didKill) { 163 | didKill = true 164 | child.kill('SIGINT') 165 | setTimeout(lock.release, 100) 166 | } else 167 | lock.release() 168 | } 169 | }) 170 | }) 171 | 172 | t.test('server process graceful exit', { 173 | skip: windows ? 'skip on windows' : false 174 | }, function (t) { 175 | var file = filename('graceful-exit') 176 | var prog = require.resolve('./fixtures/graceful-exit.js') 177 | var child = spawn(node, [prog, file]) 178 | var childClosed = false 179 | child.on('close', function (code, signal) { 180 | childClosed = true 181 | t.equal(code, 0) 182 | t.equal(signal, null) 183 | }) 184 | 185 | child.stderr.pipe(process.stderr) 186 | child.stdout.on('data', function () { 187 | // now we know that the server has the lock 188 | var didKill = false 189 | setTimeout(function () { 190 | child.kill('SIGHUP') 191 | }) 192 | 193 | var clients = [ 194 | Slocket(file, onLock), 195 | Slocket(file, onLock), 196 | Slocket(file, onLock), 197 | Slocket(file, onLock), 198 | Slocket(file, onLock) 199 | ] 200 | Promise.all(clients).then(function () { 201 | t.ok(childClosed, 'child process exited gracefully') 202 | t.end() 203 | }) 204 | 205 | function onLock (er, lock) { 206 | var has = clients.filter(function (c) { return c.has }) 207 | t.equal(has.length, 1, 'always exactly one lock') 208 | setTimeout(lock.release, 100) 209 | } 210 | }) 211 | }) 212 | 213 | t.test('server process graceful exit without release', { 214 | skip: windows ? 'skip on windows' : false 215 | }, function (t) { 216 | var file = filename('server-disconnect-graceful') 217 | var prog = require.resolve('./fixtures/exit-no-release.js') 218 | var child = spawn(node, [prog, file], { 219 | env: { NODE_DEBUG: 'slocket' } 220 | }) 221 | var childClosed = false 222 | child.on('close', function (code, signal) { 223 | childClosed = true 224 | t.equal(code, null) 225 | t.equal(signal, 'SIGHUP') 226 | }) 227 | 228 | var stderr = '' 229 | child.stderr.on('data', function (c) { 230 | stderr += c 231 | }) 232 | child.stdout.on('data', function () { 233 | // now we know that the server has the lock 234 | var didKill = false 235 | setTimeout(function () { 236 | child.kill('SIGHUP') 237 | }) 238 | 239 | var clients = [ 240 | Slocket(file, onLock), 241 | Slocket(file, onLock), 242 | Slocket(file, onLock), 243 | Slocket(file, onLock), 244 | Slocket(file, onLock) 245 | ] 246 | Promise.all(clients).then(function () { 247 | t.ok(childClosed, 'child process exited gracefully') 248 | t.match(stderr, /onProcessExit\n/, 'hit the onProcessExit fn') 249 | t.end() 250 | }) 251 | 252 | function onLock (er, lock) { 253 | var has = clients.filter(function (c) { return c.has }) 254 | t.equal(has.length, 1, 'always exactly one lock') 255 | setTimeout(lock.release, 100) 256 | } 257 | }) 258 | }) 259 | 260 | t.test('try to lock on a non-socket, auto-lock once gone', { 261 | skip: windows ? 'skip on windows' : false 262 | }, function (t) { 263 | var file = filename('not-a-socket') 264 | var fs = require('fs') 265 | fs.writeFileSync(file, 'not a socket\n') 266 | var lock = Slocket(file, function (er, lock) { 267 | lock.release() 268 | t.end() 269 | }) 270 | t.notOk(lock.has) 271 | t.notOk(fs.statSync(file).isSocket()) 272 | rimraf(file, function () {}) 273 | }) 274 | 275 | t.test('try to lock on non-Slocket socket', { 276 | skip: windows ? 'skip on windows' : false 277 | }, function (t) { 278 | var file = filename('non-slocket') 279 | var maker = require.resolve('./fixtures/abandon-socket.js') 280 | spawn(node, [maker, file]).on('close', function () { 281 | t.ok(fs.statSync(file).isSocket(), 'socket is there') 282 | var deleted = false 283 | setTimeout(function () { 284 | fs.unlinkSync(file) 285 | t.notOk(lock.has, 'should not have lock yet') 286 | deleted = true 287 | }, 100) 288 | var lock = Slocket(file, function (er, lock) { 289 | if (er) 290 | throw er 291 | t.ok(deleted, 'deleted file before lock acquired') 292 | t.equal(lock.type(), 'server') 293 | t.end() 294 | }) 295 | }) 296 | }) 297 | 298 | t.test('server disconnect, connection sync end', function (t) { 299 | var file = filename('server-disconnect-conn-sync-end') 300 | var prog = require.resolve('./fixtures/server-disconnect.js') 301 | var child = spawn(node, [prog, file]) 302 | child.stderr.pipe(process.stderr) 303 | child.stdout.on('data', function () { 304 | // now we know that the server has the lock 305 | var didKill = false 306 | setTimeout(function () { 307 | child.kill('SIGINT') 308 | }, 100) 309 | 310 | Slocket(file, function onLock (er, lock) { 311 | setTimeout(function () { 312 | lock.release(true) 313 | t.throws(fs.statSync.bind(fs, file)) 314 | t.end() 315 | }, 100) 316 | }) 317 | }) 318 | }) 319 | 320 | t.test('server kill connection abruptly', function (t) { 321 | var file = filename('server-kill-abruptly') 322 | Slocket(file, function (er, serverLock) { 323 | if (er) 324 | throw er 325 | t.equal(serverLock.type(), 'server') 326 | 327 | var clients = [ 328 | Slocket(file, onLock), 329 | Slocket(file, onLock), 330 | Slocket(file, onLock), 331 | Slocket(file, onLock), 332 | Slocket(file, onLock) 333 | ] 334 | Promise.all(clients).then(t.end) 335 | 336 | function onLock (er, lock) { 337 | var has = clients.filter(function (c) { return c.has }) 338 | t.equal(has.length, 1, 'always exactly one lock') 339 | setTimeout(lock.release, 100) 340 | } 341 | 342 | setTimeout(function () { 343 | t.equal(serverLock.currentClient, null) 344 | t.ok(serverLock.has) 345 | t.equal(serverLock.connectionQueue.length, 5) 346 | serverLock.connectionQueue[0].destroy() 347 | serverLock.connectionQueue[2].destroy() 348 | serverLock.connectionQueue[4].destroy() 349 | setTimeout(function () { 350 | t.equal(serverLock.connectionQueue.length, 5) 351 | serverLock.release() 352 | }, 100) 353 | }, 100) 354 | }) 355 | }) 356 | 357 | t.test('verify behavior when pretending to be windows', function (t) { 358 | var file = filename('windows-pretend') 359 | var locks = [ 360 | Slocket(file, onLock), 361 | Slocket(file, onLock), 362 | Slocket(file, onLock), 363 | Slocket(file, onLock), 364 | Slocket(file, onLock) 365 | ] 366 | 367 | locks.forEach(function (l) { 368 | l.windows = true 369 | }) 370 | 371 | function onLock (er, lock) { 372 | if (er) 373 | throw er 374 | 375 | // all locks are servers on windows, clients are just for waiting 376 | t.equal(lock.type(), 'server', 'is a server') 377 | var has = locks.filter(function (c) { return c.has }) 378 | t.equal(has.length, 1, 'always exactly one lock') 379 | setTimeout(lock.release, 100) 380 | } 381 | 382 | return Promise.all(locks) 383 | }) 384 | 385 | t.test('slocket.has() function', function (t) { 386 | var file = filename('not-actually-used') 387 | var s = Slocket(file) 388 | t.equal(s.type(), 'none') 389 | s.then(function () { 390 | s.release(true) 391 | s.has = true 392 | // fake a wtf state 393 | s.has = true 394 | t.equal(s.type(), 'wtf') 395 | t.end() 396 | }) 397 | }) 398 | 399 | t.test('connect to non-slocket socket', function (t) { 400 | var net = require('net') 401 | var file = filename('not-a-slocket-server') 402 | var server = net.createServer(function (conn) { 403 | conn.write('O') 404 | setTimeout(function () { 405 | conn.write('this is not ok') 406 | server.close() 407 | }) 408 | }) 409 | server.listen(file, function () { 410 | var s = Slocket(file) 411 | t.end() 412 | }) 413 | }) 414 | 415 | t.test('server object emit error after being removed') 416 | t.test('delete socket between EADDRINUSE and connect') 417 | t.test('release before connection connects') 418 | -------------------------------------------------------------------------------- /test/fixtures/abandon-socket.js: -------------------------------------------------------------------------------- 1 | var net = require('net') 2 | net.createServer(function () {}).listen(process.argv[2]).unref() 3 | -------------------------------------------------------------------------------- /test/fixtures/exit-no-release.js: -------------------------------------------------------------------------------- 1 | var lock = require('../..')(process.argv[2]) 2 | lock.then(function () { 3 | console.log("1") 4 | }) 5 | var t = setTimeout(function() {}, 10000) 6 | -------------------------------------------------------------------------------- /test/fixtures/graceful-exit.js: -------------------------------------------------------------------------------- 1 | var lock = require('../..')(process.argv[2]) 2 | lock.then(function () { 3 | console.log("1") 4 | }) 5 | var t = setTimeout(function() {}, 10000) 6 | process.on("SIGHUP", function () { 7 | lock.release() 8 | process.exit() 9 | }) 10 | -------------------------------------------------------------------------------- /test/fixtures/server-disconnect.js: -------------------------------------------------------------------------------- 1 | var lock = require('../..')(process.argv[2]) 2 | lock.then(function () { 3 | console.log("1") 4 | }) 5 | setTimeout(function() {}, 10000) 6 | process.on("SIGHUP", lock.release) 7 | --------------------------------------------------------------------------------