You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-01 16:46:54 +03:00
Standard is not as up to date and still uses a old eslint version. Instead, use the airbnb default with a couple of modifications. All required changes are included.
542 lines
20 KiB
JavaScript
542 lines
20 KiB
JavaScript
'use strict'
|
|
|
|
const { Buffer } = require('buffer')
|
|
const assert = require('assert')
|
|
const config = require('./lib/config')
|
|
const helper = require('./helper')
|
|
const utils = require('../lib/utils')
|
|
|
|
const { redis } = config
|
|
const zlib = require('zlib')
|
|
|
|
let client
|
|
|
|
describe('The \'multi\' method', () => {
|
|
afterEach(() => {
|
|
client.end(true)
|
|
})
|
|
|
|
describe('regression test', () => {
|
|
it('saved buffers with a charset different than utf-8 (issue #913)', function (done) {
|
|
this.timeout(12000) // Windows tests on 0.10 are slow
|
|
client = redis.createClient()
|
|
|
|
const end = helper.callFuncAfter(done, 100)
|
|
|
|
// Some random object created from http://beta.json-generator.com/
|
|
const testObj = {
|
|
Id: '5642c4c33d4667c4a1fefd99',
|
|
index: 0,
|
|
guid: '5baf1f1c-7621-41e7-ae7a-f8c6f3199b0f',
|
|
isActive: true,
|
|
balance: '$1,028.63',
|
|
picture: 'http://placehold.it/32x32',
|
|
age: 31,
|
|
eyeColor: 'green',
|
|
name: { first: 'Shana', last: 'Long' },
|
|
company: 'MANGLO',
|
|
email: 'shana.long@manglo.us',
|
|
phone: '+1 (926) 405-3105',
|
|
address: '747 Dank Court, Norfolk, Ohio, 1112',
|
|
about: 'Eu pariatur in nisi occaecat enim qui consequat nostrud cupidatat id. ' +
|
|
'Commodo commodo dolore esse irure minim quis deserunt anim laborum aute deserunt et est. Quis nisi laborum deserunt nisi quis.',
|
|
registered: 'Friday, April 18, 2014 9:56 AM',
|
|
latitude: '74.566613',
|
|
longitude: '-11.660432',
|
|
tags: [7, 'excepteur'],
|
|
range: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
|
friends: [3, { id: 1, name: 'Schultz Dyer' }],
|
|
greeting: 'Hello, Shana! You have 5 unread messages.',
|
|
favoriteFruit: 'strawberry'
|
|
}
|
|
|
|
function run() {
|
|
if (end() === true) {
|
|
return
|
|
}
|
|
// To demonstrate a big payload for hash set field values, let's create a big array
|
|
const testArr = []
|
|
let i = 0
|
|
for (; i < 80; i++) {
|
|
const newObj = JSON.parse(JSON.stringify(testObj))
|
|
testArr.push(newObj)
|
|
}
|
|
|
|
const json = JSON.stringify(testArr)
|
|
zlib.deflate(Buffer.from(json), (err, buffer) => {
|
|
if (err) {
|
|
done(err)
|
|
return
|
|
}
|
|
|
|
const multi = client.multi()
|
|
multi.del('SOME_KEY')
|
|
|
|
for (i = 0; i < 100; i++) {
|
|
multi.hset('SOME_KEY', `SOME_FIELD${i}`, buffer)
|
|
}
|
|
multi.exec().then(run)
|
|
})
|
|
}
|
|
run()
|
|
})
|
|
})
|
|
|
|
describe('pipeline limit', () => {
|
|
it('do not exceed maximum string size', function () {
|
|
this.timeout(process.platform !== 'win32' ? 10000 : 35000) // Windows tests are horribly slow
|
|
// Triggers a RangeError: Invalid string length if not handled properly
|
|
client = redis.createClient()
|
|
const multi = client.multi()
|
|
let i = Math.pow(2, 28)
|
|
while (i > 0) {
|
|
i -= 10230
|
|
multi.set(`foo${i}`, `bar${new Array(1024).join('1234567890')}`)
|
|
}
|
|
multi.exec().then((res) => {
|
|
assert.strictEqual(res.length, 26241)
|
|
})
|
|
return client.flushdb()
|
|
})
|
|
})
|
|
|
|
helper.allTests((ip, args) => {
|
|
describe(`using ${ip}`, () => {
|
|
describe('when not connected', () => {
|
|
beforeEach(() => {
|
|
client = redis.createClient.apply(null, args)
|
|
return client.quit()
|
|
})
|
|
|
|
it('reports an error', () => {
|
|
const multi = client.multi()
|
|
return multi.exec().then(helper.fail, helper.isError(/The connection is already closed/))
|
|
})
|
|
})
|
|
|
|
describe('when connected', () => {
|
|
beforeEach(() => {
|
|
client = redis.createClient.apply(null, args)
|
|
})
|
|
|
|
describe('monitor and transactions do not work together', () => {
|
|
it('results in a execabort', () => {
|
|
// Check that transactions in combination with monitor result in an error
|
|
return client.monitor().then(() => {
|
|
const multi = client.multi()
|
|
multi.set('hello', 'world')
|
|
return multi.exec().then(assert, (err) => {
|
|
assert.strictEqual(err.code, 'EXECABORT')
|
|
client.end(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('results in a execabort #2', () => {
|
|
// Check that using monitor with a transactions results in an error
|
|
return client.multi().set('foo', 'bar').monitor().exec()
|
|
.then(assert, (err) => {
|
|
assert.strictEqual(err.code, 'EXECABORT')
|
|
client.end(false)
|
|
})
|
|
})
|
|
|
|
it('sanity check', (done) => {
|
|
// Remove the listener and add it back again after the error
|
|
const mochaListener = helper.removeMochaListener()
|
|
process.on('uncaughtException', () => {
|
|
helper.removeMochaListener()
|
|
process.on('uncaughtException', mochaListener)
|
|
done()
|
|
})
|
|
// Check if Redis still has the error
|
|
client.monitor()
|
|
client.sendCommand('multi')
|
|
client.sendCommand('set', ['foo', 'bar'])
|
|
client.sendCommand('get', ['foo'])
|
|
client.sendCommand('exec').then((res) => {
|
|
// res[0] is going to be the monitor result of set
|
|
// res[1] is going to be the result of the set command
|
|
assert(utils.monitorRegex.test(res[0]))
|
|
assert.strictEqual(res[1].toString(), 'OK')
|
|
assert.strictEqual(res.length, 2)
|
|
client.end(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
it('executes a pipelined multi properly in combination with the offline queue', () => {
|
|
const multi1 = client.multi()
|
|
multi1.set('m1', '123')
|
|
multi1.get('m1')
|
|
const promise = multi1.exec()
|
|
assert.strictEqual(client.offlineQueue.length, 4)
|
|
return promise
|
|
})
|
|
|
|
it('executes a pipelined multi properly after a reconnect in combination with the offline queue', (done) => {
|
|
client.once('ready', () => {
|
|
client._stream.destroy()
|
|
let called = false
|
|
const multi1 = client.multi()
|
|
multi1.set('m1', '123')
|
|
multi1.get('m1')
|
|
multi1.exec().then(() => { called = true })
|
|
client.once('ready', () => {
|
|
const multi1 = client.multi()
|
|
multi1.set('m2', '456')
|
|
multi1.get('m2')
|
|
multi1.exec().then((res) => {
|
|
assert(called)
|
|
assert.strictEqual(res[1], '456')
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when connection is broken', () => {
|
|
it('return an error even if connection is in broken mode', (done) => {
|
|
client = redis.createClient({
|
|
host: 'somewhere',
|
|
port: 6379,
|
|
family: 'IPv6',
|
|
retryStrategy() {}
|
|
})
|
|
|
|
assert.strictEqual(client._connectionOptions.family, 6)
|
|
|
|
client.on('error', (err) => {
|
|
assert.strictEqual(err.code, 'NR_CLOSED')
|
|
done()
|
|
})
|
|
|
|
client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec().catch((err) => {
|
|
assert(/Stream connection ended and command aborted/.test(err.message))
|
|
assert.strictEqual(err.errors.length, 2)
|
|
assert.strictEqual(err.errors[0].args.length, 2)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when ready', () => {
|
|
beforeEach(() => {
|
|
client = redis.createClient.apply(null, args)
|
|
return client.flushdb()
|
|
})
|
|
|
|
it('returns an empty result array', () => {
|
|
const multi = client.multi()
|
|
return multi.exec().then(helper.isDeepEqual([]))
|
|
})
|
|
|
|
it('runs normal calls in-between multi commands', () => {
|
|
const multi1 = client.multi()
|
|
multi1.set('m1', '123')
|
|
return client.set('m2', '456')
|
|
})
|
|
|
|
it('runs simultaneous multi commands with the same client', () => {
|
|
const multi1 = client.multi()
|
|
multi1.set('m1', '123')
|
|
multi1.get('m1')
|
|
|
|
const multi2 = client.multi()
|
|
multi2.set('m2', '456')
|
|
multi2.get('m2')
|
|
|
|
return Promise.all([
|
|
multi1.exec(),
|
|
multi2.exec().then(helper.isDeepEqual(['OK', '456']))
|
|
])
|
|
})
|
|
|
|
it('runs simultaneous multi commands with the same client version 2', () => {
|
|
const multi2 = client.multi()
|
|
const multi1 = client.multi()
|
|
|
|
multi2.set('m2', '456')
|
|
multi1.set('m1', '123')
|
|
multi1.get('m1')
|
|
multi2.get('m1')
|
|
multi2.ping()
|
|
|
|
return Promise.all([
|
|
multi1.exec(),
|
|
multi2.exec().then(helper.isDeepEqual(['OK', '123', 'PONG']))
|
|
])
|
|
})
|
|
|
|
it('roles back a transaction when one command in a sequence of commands fails', () => {
|
|
// Provoke an error at queue time
|
|
const multi1 = client.multi()
|
|
multi1.mset('multifoo', '10', 'multibar', '20')
|
|
|
|
multi1.set('foo2')
|
|
multi1.incr('multifoo')
|
|
multi1.incr('multibar')
|
|
return multi1.exec().then(helper.fail, () => {
|
|
// Redis 2.6.5+ will abort transactions with errors
|
|
// see: http://redis.io/topics/transactions
|
|
// Confirm that the previous command, while containing an error, still worked.
|
|
const multi2 = client.multi()
|
|
multi2.incr('multibar')
|
|
multi2.incr('multifoo')
|
|
return multi2.exec().then(helper.isDeepEqual([1, 1]))
|
|
})
|
|
})
|
|
|
|
it('roles back a transaction when one command in an array of commands fails', () => {
|
|
// test nested multi-bulk replies
|
|
return client.multi([
|
|
['mget', 'multifoo', 'multibar'],
|
|
['set', 'foo2'],
|
|
['incr', 'multifoo'],
|
|
['incr', 'multibar']
|
|
]).exec().then(assert, (err) => {
|
|
assert.notEqual(err, null)
|
|
})
|
|
})
|
|
|
|
it('handles multiple operations being applied to a set', () => {
|
|
client.sadd('some set', 'mem 1')
|
|
client.sadd(['some set', 'mem 2'])
|
|
client.sadd('some set', 'mem 3')
|
|
client.sadd('some set', 'mem 4')
|
|
|
|
// make sure empty mb reply works
|
|
client.del('some missing set')
|
|
client.smembers('some missing set').then(helper.isDeepEqual([]))
|
|
|
|
// test nested multi-bulk replies with empty mb elements.
|
|
return client.multi([
|
|
['smembers', ['some set']],
|
|
['del', 'some set'],
|
|
['smembers', 'some set']
|
|
])
|
|
.scard('some set')
|
|
.exec().then((res) => {
|
|
assert.strictEqual(res[0].length, 4)
|
|
assert.strictEqual(res[1], 1)
|
|
assert.deepStrictEqual(res[2], [])
|
|
assert.strictEqual(res[3], 0)
|
|
})
|
|
})
|
|
|
|
it('allows multiple operations to be performed using constructor with all kinds of syntax', () => {
|
|
const now = Date.now()
|
|
const arr = ['multihmset', 'multibar', 'multibaz']
|
|
const arr2 = ['some manner of key', 'otherTypes']
|
|
const arr3 = [5768, 'multibarx', 'multifoox']
|
|
const arr4 = ['mset', [578, 'multibar']]
|
|
return client.multi([
|
|
arr4,
|
|
[['mset', 'multifoo2', 'multibar2', 'multifoo3', 'multibar3']],
|
|
['hmset', arr],
|
|
[['hmset', 'multihmset2', 'multibar2', 'multifoo3', 'multibar3', 'test']],
|
|
['hmset', ['multihmset', 'multibar', 'multifoo']],
|
|
['hmset', arr3],
|
|
['hmset', now, { 123456789: 'abcdefghij', 'some manner of key': 'a type of value', otherTypes: 555 }],
|
|
['hmset', 'key2', { '0123456789': 'abcdefghij', 'some manner of key': 'a type of value', otherTypes: 999 }],
|
|
['hmset', 'multihmset', ['multibar', 'multibaz']],
|
|
['hmset', 'multihmset', ['multibar', 'multibaz']]
|
|
])
|
|
.hmget(now, 123456789, 'otherTypes')
|
|
.hmget('key2', arr2)
|
|
.hmget(['multihmset2', 'some manner of key', 'multibar3'])
|
|
.mget('multifoo2', ['multifoo3', 'multifoo'])
|
|
.exec()
|
|
.then((replies) => {
|
|
assert.strictEqual(arr.length, 3)
|
|
assert.strictEqual(arr2.length, 2)
|
|
assert.strictEqual(arr3.length, 3)
|
|
assert.strictEqual(arr4.length, 2)
|
|
assert.strictEqual(replies[10][1], '555')
|
|
assert.strictEqual(replies[11][0], 'a type of value')
|
|
assert.strictEqual(replies[12][0], null)
|
|
assert.strictEqual(replies[12][1], 'test')
|
|
assert.strictEqual(replies[13][0], 'multibar2')
|
|
assert.strictEqual(replies[13].length, 3)
|
|
assert.strictEqual(replies.length, 14)
|
|
})
|
|
})
|
|
|
|
it('converts a non string key to a string', () => {
|
|
// TODO: Converting the key might change soon again.
|
|
return client.multi().hmset(true, {
|
|
test: 123,
|
|
bar: 'baz'
|
|
}).exec()
|
|
})
|
|
|
|
it('runs a multi without any further commands', () => {
|
|
return client.multi().exec().then(helper.isDeepEqual([]))
|
|
})
|
|
|
|
it('allows multiple operations to be performed using a chaining API', () => {
|
|
return client.multi()
|
|
.mset('some', '10', 'keys', '20')
|
|
.incr('some')
|
|
.incr('keys')
|
|
.mget('some', ['keys'])
|
|
.exec()
|
|
.then(helper.isDeepEqual(['OK', 11, 21, ['11', '21']]))
|
|
})
|
|
|
|
it('allows an array to be provided indicating multiple operations to perform', () => {
|
|
// test nested multi-bulk replies with nulls.
|
|
return client.multi([
|
|
['mget', ['multifoo', 'some', 'random value', 'keys']],
|
|
['incr', 'multifoo']
|
|
]).exec().then(helper.isDeepEqual([[null, null, null, null], 1]))
|
|
})
|
|
|
|
it('allows multiple operations to be performed on a hash', () => {
|
|
return client.multi()
|
|
.hmset('multihash', 'a', 'foo', 'b', 1)
|
|
.hmset('multihash', {
|
|
extra: 'fancy',
|
|
things: 'here'
|
|
})
|
|
.hgetall('multihash')
|
|
.exec()
|
|
.then((replies) => {
|
|
assert.strictEqual('OK', replies[0])
|
|
assert.strictEqual(Object.keys(replies[2]).length, 4)
|
|
assert.strictEqual('foo', replies[2].a)
|
|
assert.strictEqual('1', replies[2].b)
|
|
assert.strictEqual('fancy', replies[2].extra)
|
|
assert.strictEqual('here', replies[2].things)
|
|
})
|
|
})
|
|
|
|
it('reports EXECABORT exceptions when they occur (while queueing)', () => {
|
|
return client.multi().config('bar').set('foo').set('bar')
|
|
.exec()
|
|
.then(assert, (err) => {
|
|
assert.strictEqual(err.code, 'EXECABORT')
|
|
assert(err.message.match(/^EXECABORT/), 'Error message should begin with EXECABORT')
|
|
assert.strictEqual(err.errors.length, 2, 'err.errors should have 2 items')
|
|
assert.strictEqual(err.errors[0].command, 'SET')
|
|
assert.strictEqual(err.errors[0].code, 'ERR')
|
|
assert.strictEqual(err.errors[0].position, 1)
|
|
assert(/^ERR/.test(err.errors[0].message), 'Actual error message should begin with ERR')
|
|
})
|
|
})
|
|
|
|
it('reports multiple exceptions when they occur (while EXEC is running)', () => {
|
|
return client.multi().config('bar').debug('foo').eval('return {err=\'this is an error\'}', 0)
|
|
.exec()
|
|
.then(assert, (err) => {
|
|
assert.strictEqual(err.replies.length, 3)
|
|
assert.strictEqual(err.replies[0].code, 'ERR')
|
|
assert.strictEqual(err.replies[0].command, 'CONFIG')
|
|
assert.strictEqual(err.replies[2].code, undefined)
|
|
assert.strictEqual(err.replies[2].command, 'EVAL')
|
|
assert(/^this is an error/.test(err.replies[2].message))
|
|
assert(/^ERR/.test(err.replies[0].message), 'Error message should begin with ERR')
|
|
assert(/^ERR/.test(err.replies[1].message), 'Error message should begin with ERR')
|
|
})
|
|
})
|
|
|
|
it('should not use a transaction with execAtomic if no command is used', () => {
|
|
const multi = client.multi()
|
|
let test = false
|
|
multi.execBatch = function () {
|
|
test = true
|
|
}
|
|
multi.execAtomic()
|
|
assert(test)
|
|
})
|
|
|
|
it('should not use a transaction with execAtomic if only one command is used', () => {
|
|
const multi = client.multi()
|
|
let test = false
|
|
multi.execBatch = function () {
|
|
test = true
|
|
}
|
|
multi.set('baz', 'binary')
|
|
multi.execAtomic()
|
|
assert(test)
|
|
})
|
|
|
|
it('should use transaction with execAtomic and more than one command used', () => {
|
|
const multi = client.multi()
|
|
let test = false
|
|
multi.execBatch = function () {
|
|
test = true
|
|
}
|
|
multi.set('baz', 'binary')
|
|
multi.get('baz')
|
|
const promise = multi.execAtomic()
|
|
assert(!test)
|
|
return promise
|
|
})
|
|
|
|
it('do not mutate arguments in the multi constructor', () => {
|
|
const input = [['set', 'foo', 'bar'], ['get', 'foo']]
|
|
return client.multi(input).exec().then((res) => {
|
|
assert.strictEqual(input.length, 2)
|
|
assert.strictEqual(input[0].length, 3)
|
|
assert.strictEqual(input[1].length, 2)
|
|
})
|
|
})
|
|
|
|
it('works properly after a reconnect. issue #897', (done) => {
|
|
client._stream.destroy()
|
|
client.on('error', (err) => {
|
|
assert.strictEqual(err.code, 'ECONNREFUSED')
|
|
})
|
|
client.on('ready', () => {
|
|
client.multi([['set', 'foo', 'bar'], ['get', 'foo']]).exec().then((res) => {
|
|
assert.strictEqual(res[1], 'bar')
|
|
done()
|
|
})
|
|
})
|
|
})
|
|
|
|
it('indivdual commands work properly with multi', () => {
|
|
// Neither of the following work properly in a transactions: (This is
|
|
// due to Redis not returning the reply as expected / resulting in
|
|
// undefined behavior) (Likely there are more commands that do not
|
|
// work with a transaction)
|
|
//
|
|
// auth => can't be called after a multi command monitor => results in
|
|
// faulty return values e.g. multi().monitor().set('foo',
|
|
// 'bar').get('foo') returns ['OK, 'OK', 'monitor reply'] instead of
|
|
// ['OK', 'OK', 'bar'] quit => ends the connection before the exec
|
|
// client reply skip|off => results in weird return values. Not sure
|
|
// what exactly happens subscribe => enters subscribe mode and this
|
|
// does not work in combination with exec (the same for psubscribe,
|
|
// unsubscribe...)
|
|
//
|
|
|
|
// Make sure sendCommand is not called
|
|
client.sendCommand = () => {
|
|
throw new Error('failed')
|
|
}
|
|
|
|
assert.strictEqual(client.selectedDb, undefined)
|
|
const multi = client.multi()
|
|
multi.select(5)
|
|
// multi.client('reply', 'on') // Redis v.3.2
|
|
multi.set('foo', 'bar')
|
|
multi.info()
|
|
multi.get('foo')
|
|
return multi.exec().then((res) => {
|
|
res[2] = res[2].substr(0, 10)
|
|
assert.strictEqual(client.selectedDb, 5)
|
|
assert.deepStrictEqual(
|
|
client.serverInfo.keyspace.db5,
|
|
{ avg_ttl: 0, expires: 0, keys: 1 }
|
|
)
|
|
assert.deepStrictEqual(res, ['OK', 'OK', '# Server\r\n', 'bar'])
|
|
return client.flushdb()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|