Skip to content

Commit

Permalink
Merge pull request #119 from watson/unix-socket
Browse files Browse the repository at this point in the history
Use autocannon on a UNIX socket
  • Loading branch information
GlenTiki authored Jan 29, 2018
2 parents 4a38be5 + 3c802ab commit 72102ff
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 12 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ npm i autocannon --save
```
Usage: autocannon [opts] URL
URL is any valid http or https url.
URL is any valid http or https url. Can alternatively be a path to a
Unix Domain socket.
Available options:
Expand Down Expand Up @@ -116,7 +117,8 @@ autocannon({
Start autocannon against the given target.

* `opts`: Configuration options for the autocannon instance. This can have the following attributes. _REQUIRED_.
* `url`: The given target. Can be http or https. _REQUIRED_.
* `url`: The given target. Can be http or https. _REQUIRED unless `socketPath` is provided_.
* `socketPath`: Unix Domain Socket (use one of `url` or `socketPath`). _REQUIRED unless `url` is provided_.
* `connections`: The number of concurrent connections. _OPTIONAL_ default: `10`.
* `duration`: The number of seconds to run the autocannon. Can be a [timestring](https://www.npmjs.com/package/timestring). _OPTIONAL_ default: `10`.
* `amount`: A `Number` stating the amount of requests to make before ending the test. This overrides duration and takes precedence, so the test won't end until the amount of requests needed to be completed are completed. _OPTIONAL_.
Expand Down
12 changes: 10 additions & 2 deletions autocannon.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ function parseArguments (argvs) {
}
})

argv.url = argv._[0]
if (isUnixSocket(argv._[0])) {
argv.socketPath = argv._[0]
} else {
argv.url = argv._[0]
}

// support -n to disable the progress bar and results table
if (argv.n) {
Expand All @@ -72,7 +76,7 @@ function parseArguments (argvs) {
return
}

if (!argv.url || argv.help) {
if (!(argv.url || argv.socketPath) || argv.help) {
console.error(help)
return
}
Expand Down Expand Up @@ -124,6 +128,10 @@ function start (argv) {
})
}

function isUnixSocket (path) {
return /\.sock$/.test(path) && fs.existsSync(path)
}

if (require.main === module) {
start(parseArguments(process.argv.slice(2)))
}
2 changes: 1 addition & 1 deletion help.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Usage: autocannon [opts] URL

URL is any valid http or https url.
URL is any valid http or https url. Can alternatively be a path to a Unix Domain socket.

Available options:

Expand Down
13 changes: 11 additions & 2 deletions lib/httpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function Client (opts) {

this.opts = opts
this.timeout = (opts.timeout || 10) * 1000
this.unixSocket = !!opts.socketPath
this.secure = opts.protocol === 'https:'
if (this.secure && this.opts.port === 80) this.opts.port = 443
this.parser = new HTTPParser(HTTPParser.RESPONSE)
Expand Down Expand Up @@ -102,9 +103,17 @@ inherits(Client, EE)

Client.prototype._connect = function () {
if (this.secure) {
this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false })
if (this.unixSocket) {
this.conn = tls.connect(this.opts.socketPath, { rejectUnauthorized: false })
} else {
this.conn = tls.connect(this.opts.port, this.opts.hostname, { rejectUnauthorized: false })
}
} else {
this.conn = net.connect(this.opts.port, this.opts.hostname)
if (this.unixSocket) {
this.conn = net.connect(this.opts.socketPath)
} else {
this.conn = net.connect(this.opts.port, this.opts.hostname)
}
}

this.conn.on('error', (error) => {
Expand Down
11 changes: 7 additions & 4 deletions lib/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ function run (opts, cb) {
// is done
tracker.opts = opts

if (opts.url.indexOf('http') !== 0) opts.url = 'http://' + opts.url
const url = URL.parse(opts.url)
const unixSocket = !!opts.socketPath

if (!unixSocket && opts.url.indexOf('http') !== 0) opts.url = 'http://' + opts.url
const url = unixSocket ? {socketPath: opts.socketPath} : URL.parse(opts.url)

let counter = 0
let bytes = 0
Expand Down Expand Up @@ -143,6 +145,7 @@ function run (opts, cb) {
let result = {
title: opts.title,
url: opts.url,
socketPath: opts.socketPath,
requests: histAsObj(requests, totalCompletedRequests),
latency: addPercentiles(latencies, histAsObj(latencies)),
throughput: histAsObj(throughput, totalBytes),
Expand Down Expand Up @@ -249,8 +252,8 @@ function run (opts, cb) {

// will return true if error with opts entered
function checkOptsForErrors () {
if (!opts.url) {
errorCb(new Error('url option required'))
if (!opts.url && !opts.socketPath) {
errorCb(new Error('url or socketPath option required'))
return true
}

Expand Down
2 changes: 1 addition & 1 deletion test/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function startServer (opts) {

server.on('connection', () => { server.autocannonConnects++ })

server.listen(0)
server.listen(opts.socketPath || 0)

function handle (req, res) {
res.statusCode = statusCode
Expand Down
26 changes: 26 additions & 0 deletions test/run.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict'

const os = require('os')
const path = require('path')
const test = require('tap').test
const run = require('../lib/run')
const helper = require('./helper')
Expand Down Expand Up @@ -222,6 +224,30 @@ test('run should recognise valid urls without http at the start', (t) => {
})
})

if (process.platform !== 'win32') {
test('run should accept a unix socket instead of a url', (t) => {
t.plan(11)

const socketPath = path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock')
helper.startServer({socketPath})

run({socketPath}, (err, result) => {
t.error(err)
t.ok(result, 'results should exist')
t.equal(result.socketPath, socketPath, 'socketPath should be included in result')
t.ok(result.requests.total > 0, 'should make at least one request')
t.equal(result.errors, 0, 'no errors')
t.equal(result['1xx'], 0, '1xx codes')
t.equal(result['2xx'], result.requests.total, '2xx codes')
t.equal(result['3xx'], 0, '3xx codes')
t.equal(result['4xx'], 0, '4xx codes')
t.equal(result['5xx'], 0, '5xx codes')
t.equal(result.non2xx, 0, 'non 2xx codes')
t.end()
})
})
}

for (let i = 1; i <= 5; i++) {
test(`run should count all ${i}xx status codes`, (t) => {
t.plan(2)
Expand Down
48 changes: 48 additions & 0 deletions test/unixSocket.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict'

// UNIX sockets are not supported by Windows
if (process.platform === 'win32') process.exit(0)

const t = require('tap')
const split = require('split2')
const os = require('os')
const path = require('path')
const childProcess = require('child_process')
const helper = require('./helper')

const lines = [
/Running 1s test @ .*$/,
/10 connections.*$/,
/$/,
/Stat.*Avg.*Stdev.*Max.*$/,
/Latency \(ms\).*$/,
/Req\/Sec.*$/,
/Bytes\/Sec.*$/,
/$/,
/.* requests in \d+s, .* read/
]

t.plan(lines.length * 2)

const socketPath = path.join(os.tmpdir(), 'autocannon-' + Date.now() + '.sock')
helper.startServer({socketPath})

const child = childProcess.spawn(process.execPath, [path.join(__dirname, '..'), '-d', '1', socketPath], {
cwd: __dirname,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
})

t.tearDown(() => {
child.kill()
})

child
.stderr
.pipe(split())
.on('data', (line) => {
const regexp = lines.shift()
t.ok(regexp, 'we are expecting this line')
t.ok(regexp.test(line), 'line matches ' + regexp)
})

0 comments on commit 72102ff

Please sign in to comment.