You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
8.0 KiB
299 lines
8.0 KiB
'use strict'; |
|
var _ = require('lodash'); |
|
var Commander = require('./commander'); |
|
var Command = require('./command'); |
|
var fbuffer = require('flexbuffer'); |
|
var Promise = require('bluebird'); |
|
var utils = require('./utils'); |
|
var commands = require('../commands'); |
|
|
|
function Pipeline(redis) { |
|
Commander.call(this); |
|
|
|
this.redis = redis; |
|
this.isCluster = this.redis.constructor.name === 'Cluster'; |
|
this.options = redis.options; |
|
this._queue = []; |
|
this._result = []; |
|
this._transactions = 0; |
|
this._shaToScript = {}; |
|
|
|
var _this = this; |
|
_.keys(redis.scriptsSet).forEach(function (name) { |
|
var script = redis.scriptsSet[name]; |
|
_this._shaToScript[script.sha] = script; |
|
_this[name] = redis[name]; |
|
_this[name + 'Buffer'] = redis[name + 'Buffer']; |
|
}); |
|
|
|
this.promise = new Promise(function (resolve, reject) { |
|
_this.resolve = resolve; |
|
_this.reject = reject; |
|
}); |
|
} |
|
|
|
_.assign(Pipeline.prototype, Commander.prototype); |
|
|
|
Pipeline.prototype.fillResult = function (value, position) { |
|
this._result[position] = value; |
|
|
|
if (--this.replyPending) { |
|
return; |
|
} |
|
|
|
var i; |
|
if (this.isCluster) { |
|
var retriable = true; |
|
var commonError; |
|
var inTransaction; |
|
for (i = 0; i < this._result.length; ++i) { |
|
var error = this._result[i][0]; |
|
var command = this._queue[i]; |
|
if (command.name === 'multi') { |
|
inTransaction = true; |
|
} else if (command.name === 'exec') { |
|
inTransaction = false; |
|
} |
|
if (error) { |
|
if (command.name === 'exec' && error.message === 'EXECABORT Transaction discarded because of previous errors.') { |
|
continue; |
|
} |
|
if (!commonError) { |
|
commonError = { |
|
name: error.name, |
|
message: error.message |
|
}; |
|
} else if (commonError.name !== error.name || commonError.message !== error.message) { |
|
retriable = false; |
|
break; |
|
} |
|
} else if (!inTransaction) { |
|
var commandDef = commands[command.name]; |
|
var isReadOnly = commandDef && _.include(commandDef.flags, 'readonly'); |
|
if (!isReadOnly) { |
|
retriable = false; |
|
break; |
|
} |
|
} |
|
} |
|
if (commonError && retriable) { |
|
var _this = this; |
|
var errv = commonError.message.split(' '); |
|
var queue = this._queue; |
|
inTransaction = false; |
|
this._queue = []; |
|
for (i = 0; i < queue.length; ++i) { |
|
if (errv[0] === 'ASK' && !inTransaction && |
|
queue[i].name !== 'asking' && |
|
(!queue[i - 1] || queue[i - 1].name !== 'asking')) { |
|
var asking = new Command('asking'); |
|
asking.ignore = true; |
|
this.sendCommand(asking); |
|
} |
|
queue[i].initPromise(); |
|
this.sendCommand(queue[i]); |
|
if (queue[i].name === 'multi') { |
|
inTransaction = true; |
|
} else if (queue[i].name === 'exec') { |
|
inTransaction = false; |
|
} |
|
} |
|
|
|
var matched = true; |
|
if (typeof this.leftRedirections === 'undefined') { |
|
this.leftRedirections = {}; |
|
} |
|
this.redis.handleError(commonError, this.leftRedirections, { |
|
moved: function (node) { |
|
_this.preferNode = node; |
|
_this.redis.slots[errv[1]] = node; |
|
_this.redis.refreshSlotsCache(); |
|
_this.exec(); |
|
}, |
|
ask: function (node) { |
|
_this.preferNode = node; |
|
_this.exec(); |
|
}, |
|
clusterDown: function () { |
|
_this.exec(); |
|
}, |
|
connectionClosed: function () { |
|
_this.exec(); |
|
}, |
|
maxRedirections: function () { |
|
matched = false; |
|
}, |
|
defaults: function () { |
|
matched = false; |
|
} |
|
}); |
|
if (matched) { |
|
return; |
|
} |
|
} |
|
} |
|
|
|
var ignoredCount = 0; |
|
for (i = 0; i < this._queue.length - ignoredCount; ++i) { |
|
if (this._queue[i + ignoredCount].ignore) { |
|
ignoredCount += 1; |
|
} |
|
this._result[i] = this._result[i + ignoredCount]; |
|
} |
|
this.resolve(this._result.slice(0, this._result.length - ignoredCount)); |
|
}; |
|
|
|
Pipeline.prototype.sendCommand = function (command) { |
|
var position = this._queue.length; |
|
|
|
var _this = this; |
|
|
|
command.promise.then(function (result) { |
|
_this.fillResult([null, result], position); |
|
}).catch(function (error) { |
|
_this.fillResult([error], position); |
|
}); |
|
|
|
this._queue.push(command); |
|
|
|
return this; |
|
}; |
|
|
|
Pipeline.prototype.addBatch = function (commands) { |
|
for (var i = 0; i < commands.length; ++i) { |
|
var command = commands[i]; |
|
var commandName = command.shift(); |
|
this[commandName].apply(this, command); |
|
} |
|
|
|
return this; |
|
}; |
|
|
|
var multi = Pipeline.prototype.multi; |
|
Pipeline.prototype.multi = function () { |
|
this._transactions += 1; |
|
return multi.apply(this, arguments); |
|
}; |
|
|
|
var execBuffer = Pipeline.prototype.execBuffer; |
|
Pipeline.prototype.execBuffer = function () { |
|
if (this._transactions > 0) { |
|
this._transactions -= 1; |
|
} |
|
return execBuffer.apply(this, arguments); |
|
}; |
|
|
|
var exec = Pipeline.prototype.exec; |
|
Pipeline.prototype.exec = function (callback) { |
|
if (this._transactions > 0) { |
|
this._transactions -= 1; |
|
return exec.apply(this, arguments); |
|
} |
|
if (!this.nodeifiedPromise) { |
|
this.nodeifiedPromise = true; |
|
this.promise.nodeify(callback); |
|
} |
|
if (_.isEmpty(this._queue)) { |
|
this.resolve([]); |
|
} |
|
var pipelineSlot; |
|
// Check whether scripts exists and get a sampleKey. |
|
var scripts = []; |
|
for (var i = 0; i < this._queue.length; ++i) { |
|
var item = this._queue[i]; |
|
if (this.isCluster) { |
|
var keys = item.getKeys(); |
|
for (var j = 0; j < keys.length; ++j) { |
|
var slot = utils.calcSlot(keys[j]); |
|
if (typeof pipelineSlot === 'undefined') { |
|
pipelineSlot = slot; |
|
} |
|
if (pipelineSlot !== slot) { |
|
this.reject(new Error('All keys in the pipeline should belong to the same slot(expect "' + |
|
keys[j] + '" belongs to slot ' + pipelineSlot + ').')); |
|
return this.promise; |
|
} |
|
} |
|
} |
|
if (this.isCluster && item.isCustomCommand) { |
|
this.reject(new Error('Sending custom commands in pipeline is not supported in Cluster mode.')); |
|
return this.promise; |
|
} |
|
if (item.name !== 'evalsha') { |
|
continue; |
|
} |
|
var script = this._shaToScript[item.args[0]]; |
|
if (!script) { |
|
continue; |
|
} |
|
scripts.push(script); |
|
} |
|
if (this.isCluster && typeof pipelineSlot === 'undefined') { |
|
pipelineSlot = Math.random() * 16384 | 0; |
|
} |
|
|
|
var _this = this; |
|
if (!scripts.length) { |
|
return execPipeline(); |
|
} |
|
return this.redis.script('exists', scripts.map(function (item) { |
|
return item.sha; |
|
})).then(function (results) { |
|
var pending = []; |
|
for (var i = 0; i < results.length; ++i) { |
|
if (!results[i]) { |
|
pending.push(scripts[i]); |
|
} |
|
} |
|
return Promise.all(pending.map(function (script) { |
|
return _this.redis.script('load', script.lua); |
|
})); |
|
}).then(execPipeline); |
|
|
|
function execPipeline() { |
|
var data = ''; |
|
var writePending = _this.replyPending = _this._queue.length; |
|
|
|
var node = { slot: pipelineSlot, redis: _this.preferNode }; |
|
var bufferMode = false; |
|
var stream = { |
|
write: function (writable) { |
|
if (writable instanceof Buffer) { |
|
bufferMode = true; |
|
} |
|
if (bufferMode) { |
|
if (typeof data === 'string') { |
|
var flexBuffer = new fbuffer.FlexBuffer(0); |
|
flexBuffer.write(data); |
|
data = flexBuffer; |
|
} |
|
data.write(writable); |
|
} else { |
|
data += writable; |
|
} |
|
if (!--writePending) { |
|
if (bufferMode) { |
|
data = data.getBuffer(); |
|
} |
|
if (_this.isCluster) { |
|
node.redis.stream.write(data); |
|
} else { |
|
_this.redis.stream.write(data); |
|
} |
|
|
|
// Reset writePending for resending |
|
writePending = _this._queue.length; |
|
data = ''; |
|
bufferMode = false; |
|
} |
|
} |
|
}; |
|
|
|
for (var i = 0; i < _this._queue.length; ++i) { |
|
_this.redis.sendCommand(_this._queue[i], stream, node); |
|
} |
|
return _this.promise; |
|
} |
|
}; |
|
|
|
module.exports = Pipeline;
|
|
|