Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support RESP3 #50

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ addons:
packages:
- g++-4.8
node_js:
# We do support Node.js v4 but tests are not run against it anymore since the testing frameworks do not support it anymore.
# - "4"
- "6"
- "7"
- "8"
- "9"
- "10"
- "11"
install:
- npm install
- npm install hiredis
Expand Down
47 changes: 37 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

# redis-parser

A high performance javascript redis parser built for [node_redis](https://github.com/NodeRedis/node_redis) and [ioredis](https://github.com/luin/ioredis). Parses all [RESP](http://redis.io/topics/protocol) data.
A high performance javascript RESP v2/v3 parser built for
[node_redis](https://github.com/NodeRedis/node_redis) and
[ioredis](https://github.com/luin/ioredis). Parses all
[RESP v2](http://redis.io/topics/protocol) and
[RESP v3](https://github.com/antirez/RESP3) data.

## Install

Expand All @@ -24,23 +28,30 @@ const myParser = new Parser(options);

* `returnReply`: *function*; mandatory
* `returnError`: *function*; mandatory
* `returnPush`: *function*; optional (mandatory for RESP3)
* `returnFatalError`: *function*; optional, defaults to the returnError function
* `returnBuffers`: *boolean*; optional, defaults to false
* `stringNumbers`: *boolean*; optional, defaults to false
* `bigInt`: *boolean*; optional, defaults to false

### Functions

* `reset()`: reset the parser to it's initial state
* `setReturnBuffers(boolean)`: set the returnBuffers option on/off without resetting the parser
* `setStringNumbers(boolean)`: set the stringNumbers option on/off without resetting the parser
* `setReturnBuffers(boolean)`: set the returnBuffers option on/off without
resetting the parser
* `setStringNumbers(boolean)`: set the stringNumbers option on/off without
resetting the parser
* `setBigInt(boolean)`: set the bigInt option on/off without
resetting the parser

### Error classes

* `RedisError` sub class of Error
* `ReplyError` sub class of RedisError
* `ParserError` sub class of RedisError

All Redis errors will be returned as `ReplyErrors` while a parser error is returned as `ParserError`.
All Redis errors will be returned as `ReplyErrors` while a parser error is
returned as `ParserError`.
All error classes can be imported by the npm `redis-errors` package.

### Example
Expand Down Expand Up @@ -78,11 +89,18 @@ const parser = new Parser({
});
```

You do not have to use the returnFatalError function. Fatal errors will be returned in the normal error function in that case.
You do not have to use the returnFatalError function. Fatal errors will be
returned in the normal error function in that case.

And if you want to return buffers instead of strings, you can do this by adding the `returnBuffers` option.
And if you want to return buffers instead of strings, you can do this by adding
the `returnBuffers` option.

If you handle with big numbers that are to large for JS (Number.MAX_SAFE_INTEGER === 2^53 - 16) please use the `stringNumbers` option. That way all numbers are going to be returned as String and you can handle them safely.
If you handle with big numbers that are to large for JS (Number.MAX_SAFE_INTEGER
=== 2^53 - 16) please use the `bigInt` or the `stringNumbers` option. That way
all numbers are going to be returned as bigint or string and you can handle them
safely. `bigInt` is going to be slower than returning strings due to the extra
conversion and it only works for integers. `RESP3` also uses 64bit doubles that can
not be represented by `bigint`.

```js
// Same functions as in the first example
Expand All @@ -95,19 +113,28 @@ const parser = new Parser({
lib.returnError(err);
},
returnBuffers: true, // All strings are returned as Buffer e.g. <Buffer 48 65 6c 6c 6f>
stringNumbers: true // All numbers are returned as String
bigInt: true // All integers are returned as BigInt
});

// The streamHandler as above
```

## Protocol errors

To handle protocol errors (this is very unlikely to happen) gracefully you should add the returnFatalError option, reject any still running command (they might have been processed properly but the reply is just wrong), destroy the socket and reconnect. Note that while doing this no new command may be added, so all new commands have to be buffered in the meantime, otherwise a chunk might still contain partial data of a following command that was already processed properly but answered in the same chunk as the command that resulted in the protocol error.
To handle protocol errors (this is very unlikely to happen) gracefully you
should add the returnFatalError option, reject any still running command (they
might have been processed properly but the reply is just wrong), destroy the
socket and reconnect. Note that while doing this no new command may be added, so
all new commands have to be buffered in the meantime, otherwise a chunk might
still contain partial data of a following command that was already processed
properly but answered in the same chunk as the command that resulted in the
protocol error.

## Contribute

The parser is highly optimized but there may still be further optimizations possible.
The parser is highly optimized but there may still be further optimizations
possible. Especially the new aggregated data types from RESP3 should have some
optimization potential left.

npm install
npm test
Expand Down
104 changes: 18 additions & 86 deletions benchmark/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const Benchmark = require('benchmark')
const suite = new Benchmark.Suite()
const Parser = require('./../')
const Buffer = require('buffer').Buffer
const HiredisParser = require('../test/hiredis')

function returnError (error) {}
function checkReply (error, res) {}
Expand Down Expand Up @@ -64,25 +63,20 @@ const options = {
returnFatalError: returnError
}
const parser = new Parser(options)
const parserHiRedis = new HiredisParser(options)

options.returnBuffers = true
const parserBuffer = new Parser(options)
const parserHiRedisBuffer = new HiredisParser(options)

delete options.returnBuffers
options.stringNumbers = true
const parserStr = new Parser(options)

// BULK STRINGS
delete options.stringNumbers
options.bigInt = true
const parserBigInt = new Parser(options)

suite.add('HIREDIS: $ multiple chunks in a bulk string', function () {
parserHiRedis.execute(startBuffer)
parserHiRedis.execute(chunkBuffer)
parserHiRedis.execute(chunkBuffer)
parserHiRedis.execute(chunkBuffer)
parserHiRedis.execute(endBuffer)
})
const runBigInt = process.argv.length === 2 || process.argv.includes('bigint')

// BULK STRINGS

suite.add('JS PARSER: $ multiple chunks in a bulk string', function () {
parser.execute(startBuffer)
Expand All @@ -92,14 +86,6 @@ suite.add('JS PARSER: $ multiple chunks in a bulk string', function () {
parser.execute(endBuffer)
})

suite.add('HIREDIS BUF: $ multiple chunks in a bulk string', function () {
parserHiRedisBuffer.execute(startBuffer)
parserHiRedisBuffer.execute(chunkBuffer)
parserHiRedisBuffer.execute(chunkBuffer)
parserHiRedisBuffer.execute(chunkBuffer)
parserHiRedisBuffer.execute(endBuffer)
})

suite.add('JS PARSER BUF: $ multiple chunks in a bulk string', function () {
parserBuffer.execute(startBuffer)
parserBuffer.execute(chunkBuffer)
Expand All @@ -110,36 +96,18 @@ suite.add('JS PARSER BUF: $ multiple chunks in a bulk string', function () {

// CHUNKED STRINGS

suite.add('\nHIREDIS: + multiple chunks in a string', function () {
parserHiRedis.execute(chunkedStringPart1)
parserHiRedis.execute(chunkedStringPart2)
})

suite.add('JS PARSER: + multiple chunks in a string', function () {
parser.execute(chunkedStringPart1)
parser.execute(chunkedStringPart2)
})

suite.add('HIREDIS BUF: + multiple chunks in a string', function () {
parserHiRedisBuffer.execute(chunkedStringPart1)
parserHiRedisBuffer.execute(chunkedStringPart2)
})

suite.add('JS PARSER BUF: + multiple chunks in a string', function () {
parserBuffer.execute(chunkedStringPart1)
parserBuffer.execute(chunkedStringPart2)
})

// BIG BULK STRING

suite.add('\nHIREDIS: $ 4mb bulk string', function () {
parserHiRedis.execute(startBigBuffer)
for (var i = 0; i < 64; i++) {
parserHiRedis.execute(chunks[i])
}
parserHiRedis.execute(endBuffer)
})

suite.add('JS PARSER: $ 4mb bulk string', function () {
parser.execute(startBigBuffer)
for (var i = 0; i < 64; i++) {
Expand All @@ -148,14 +116,6 @@ suite.add('JS PARSER: $ 4mb bulk string', function () {
parser.execute(endBuffer)
})

suite.add('HIREDIS BUF: $ 4mb bulk string', function () {
parserHiRedisBuffer.execute(startBigBuffer)
for (var i = 0; i < 64; i++) {
parserHiRedisBuffer.execute(chunks[i])
}
parserHiRedisBuffer.execute(endBuffer)
})

suite.add('JS PARSER BUF: $ 4mb bulk string', function () {
parserBuffer.execute(startBigBuffer)
for (var i = 0; i < 64; i++) {
Expand All @@ -166,28 +126,16 @@ suite.add('JS PARSER BUF: $ 4mb bulk string', function () {

// STRINGS

suite.add('\nHIREDIS: + simple string', function () {
parserHiRedis.execute(stringBuffer)
})

suite.add('JS PARSER: + simple string', function () {
parser.execute(stringBuffer)
})

suite.add('HIREDIS BUF: + simple string', function () {
parserHiRedisBuffer.execute(stringBuffer)
})

suite.add('JS PARSER BUF: + simple string', function () {
parserBuffer.execute(stringBuffer)
})

// INTEGERS

suite.add('\nHIREDIS: : integer', function () {
parserHiRedis.execute(integerBuffer)
})

suite.add('JS PARSER: : integer', function () {
parser.execute(integerBuffer)
})
Expand All @@ -196,11 +144,13 @@ suite.add('JS PARSER STR: : integer', function () {
parserStr.execute(integerBuffer)
})

// BIG INTEGER
if (runBigInt) {
suite.add('JS PARSER BIGINT: : integer', function () {
parserBigInt.execute(integerBuffer)
})
}

suite.add('\nHIREDIS: : big integer', function () {
parserHiRedis.execute(bigIntegerBuffer)
})
// BIG INTEGER

suite.add('JS PARSER: : big integer', function () {
parser.execute(bigIntegerBuffer)
Expand All @@ -210,44 +160,30 @@ suite.add('JS PARSER STR: : big integer', function () {
parserStr.execute(bigIntegerBuffer)
})

// ARRAYS
if (runBigInt) {
suite.add('JS PARSER BIGINT: : big integer', function () {
parserBigInt.execute(bigIntegerBuffer)
})
}

suite.add('\nHIREDIS: * array', function () {
parserHiRedis.execute(arrayBuffer)
})
// ARRAYS

suite.add('JS PARSER: * array', function () {
parser.execute(arrayBuffer)
})

suite.add('HIREDIS BUF: * array', function () {
parserHiRedisBuffer.execute(arrayBuffer)
})

suite.add('JS PARSER BUF: * array', function () {
parserBuffer.execute(arrayBuffer)
})

// BIG NESTED ARRAYS

suite.add('\nHIREDIS: * big nested array', function () {
for (var i = 0; i < bigArrayChunks.length; i++) {
parserHiRedis.execute(bigArrayChunks[i])
}
})

suite.add('JS PARSER: * big nested array', function () {
for (var i = 0; i < bigArrayChunks.length; i++) {
parser.execute(bigArrayChunks[i])
}
})

suite.add('HIREDIS BUF: * big nested array', function () {
for (var i = 0; i < bigArrayChunks.length; i++) {
parserHiRedisBuffer.execute(bigArrayChunks[i])
}
})

suite.add('JS PARSER BUF: * big nested array', function () {
for (var i = 0; i < bigArrayChunks.length; i++) {
parserBuffer.execute(bigArrayChunks[i])
Expand All @@ -256,10 +192,6 @@ suite.add('JS PARSER BUF: * big nested array', function () {

// ERRORS

suite.add('\nHIREDIS: - error', function () {
parserHiRedis.execute(errorBuffer)
})

suite.add('JS PARSER: - error', function () {
parser.execute(errorBuffer)
})
Expand Down
Loading