├── index.js ├── test ├── box2.lua ├── box1.lua ├── box.lua └── app.js ├── .gitignore ├── .travis.yml ├── benchmark ├── box.lua ├── write.js └── read.js ├── test.sh ├── .eslintrc ├── lib ├── connector.js ├── const.js ├── utils.js ├── sliderBuffer.js ├── pipeline.js ├── msgpack-extensions.js ├── parser.js ├── event-handler.js ├── connection.js └── commands.js ├── package.json └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/connection.js'); -------------------------------------------------------------------------------- /test/box2.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | box.cfg{listen=33015} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules/ 3 | node_modules/* 4 | .idea/ 5 | .idea/* 6 | *.snap 7 | *.xlog 8 | 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /test/box1.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | box.cfg{listen=33014} 3 | 4 | if not box.schema.user.exists('test') then 5 | box.schema.user.create('test', {password = 'test'}) 6 | box.schema.user.grant('test', 'execute', 'universe') 7 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | 5 | language: node_js 6 | node_js: 7 | - "5" 8 | - "4" 9 | - "6" 10 | - "7" 11 | - "node" 12 | - "iojs" 13 | cache: false 14 | 15 | before_script: 16 | - ./test.sh 17 | 18 | script: 19 | - sleep 4 20 | - npm test 21 | 22 | after_success: 23 | - npm run coveralls -------------------------------------------------------------------------------- /benchmark/box.lua: -------------------------------------------------------------------------------- 1 | box.cfg{listen=3301} 2 | 3 | if not box.schema.user.exists('test') then 4 | box.schema.user.create('test') 5 | end 6 | 7 | user = box.user 8 | if not user then 9 | box.schema.user.grant('test', 'execute', 'universe') 10 | end 11 | 12 | box.once('grant_user_right', function() 13 | box.schema.user.grant('guest', 'read,write,execute', 'universe') 14 | end) 15 | 16 | c = box.space.counter 17 | if not c then 18 | c = box.schema.space.create('counter') 19 | pr = c:create_index('primary', {type = 'TREE', unique = true, parts = {1, 'STR'}}) 20 | c:insert({'test', 1337, 'Some text.'}) 21 | end 22 | 23 | s = box.space.bench 24 | if not s then 25 | s = box.schema.space.create('bench') 26 | p = s:create_index('primary', {type = 'hash', parts = {1, 'num'}}) 27 | end 28 | 29 | function clear() 30 | box.session.su('admin') 31 | box.space.bench:truncate{} 32 | end -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # curl http://tarantool.org/dist/public.key | sudo apt-key add - 4 | # echo "deb http://tarantool.org/dist/master/ubuntu/ `lsb_release -c -s` main" | sudo tee -a /etc/apt/sources.list.d/tarantool.list 5 | # sudo apt-get update > /dev/null 6 | sudo docker pull tarantool/tarantool:1.7 7 | sudo docker run --name tarantool -p33013:33013 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box.lua 8 | sudo docker run --name reserve -p33014:33014 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box1.lua 9 | sudo docker run --name reserve_2 -p33015:33015 -d -v `pwd`/test:/opt/tarantool tarantool/tarantool:1.7 tarantool /opt/tarantool/box2.lua 10 | npm run test 11 | sudo docker stop -t 2 tarantool 12 | sudo docker rm tarantool 13 | sudo docker stop -t 2 reserve 14 | sudo docker rm reserve 15 | sudo docker stop -t 2 reserve_2 16 | sudo docker rm reserve_2 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 6, 7 | "ecmaFeatures": {} 8 | }, 9 | "rules": { 10 | "block-scoped-var": 2, 11 | "no-cond-assign": 2, 12 | "no-control-regex": 2, 13 | "no-debugger": 2, 14 | "no-dupe-args": 2, 15 | "no-dupe-keys": 2, 16 | "no-duplicate-case": 2, 17 | "no-ex-assign": 2, 18 | "no-extra-semi": 2, 19 | "no-func-assign": 2, 20 | "no-invalid-regexp": 2, 21 | "no-irregular-whitespace": 2, 22 | "no-negated-in-lhs": 2, 23 | "no-obj-calls": 2, 24 | "no-redeclare": 2, 25 | "no-regex-spaces": 2, 26 | "no-sparse-arrays": 2, 27 | "no-unexpected-multiline": 2, 28 | "no-unreachable": 2, 29 | "no-delete-var": 2, 30 | "no-shadow": 2, 31 | "no-undef": 2, 32 | "radix": 2, 33 | "semi": 2, 34 | "use-isnan": 2, 35 | "valid-jsdoc": 2, 36 | "valid-typeof": 2 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/connector.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | var net = require('net'); 3 | var tls = require('tls'); 4 | var { TarantoolError } = require('./utils'); 5 | 6 | function Connector(options) { 7 | this.options = options; 8 | } 9 | 10 | Connector.prototype.disconnect = function () { 11 | this.connecting = false; 12 | if (this.socket) { 13 | this.socket.end(); 14 | } 15 | }; 16 | 17 | Connector.prototype.connect = function (callback) { 18 | this.connecting = true; 19 | 20 | var _this = this; 21 | process.nextTick(function () { 22 | if (!_this.connecting) { 23 | callback(new TarantoolError('Connection is closed.')); 24 | return; 25 | } 26 | try { 27 | var connectionModule 28 | if (typeof _this.options.tls == 'object') { 29 | connectionModule = tls 30 | _this.options = Object.assign(_this.options, _this.options.tls) 31 | } else { 32 | connectionModule = net 33 | } 34 | _this.socket = connectionModule.connect(_this.options); 35 | } catch (err) { 36 | callback(err); 37 | return; 38 | } 39 | callback(null, _this.socket); 40 | }); 41 | }; 42 | 43 | module.exports = Connector; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarantool-driver", 3 | "version": "3.1.0", 4 | "description": "Tarantool driver for 1.7+", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint ./lib ./test", 8 | "test": "istanbul cover --report lcov _mocha", 9 | "coveralls": "cat ./coverage/lcov.info | coveralls", 10 | "benchmark-read": "node benchmark/read", 11 | "benchmark-write": "node benchmark/write" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/tarantool/node-tarantool-driver.git" 16 | }, 17 | "keywords": [ 18 | "tarantool", 19 | "driver", 20 | "db", 21 | "msgpack", 22 | "tarantool client", 23 | "tarantool connector", 24 | "in memory", 25 | "in-memory", 26 | "in memory storage", 27 | "in-memory storage", 28 | "in memory database", 29 | "in-memory database", 30 | "database" 31 | ], 32 | "author": "KlonD90", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/tarantool/node-tarantool-driver/issues" 36 | }, 37 | "homepage": "https://github.com/tarantool/node-tarantool-driver", 38 | "dependencies": { 39 | "debug": "^2.6.8", 40 | "denque": "^1.2.1", 41 | "int64-buffer": "^1.0.1", 42 | "lodash": "^4.17.4", 43 | "msgpack-lite": "^0.1.20", 44 | "uuid": "^9.0.0" 45 | }, 46 | "devDependencies": { 47 | "benchmark": "^2.1.4", 48 | "chai": "^4.1.0", 49 | "coveralls": "^2.13.3", 50 | "eslint": "^2.5.3", 51 | "ioredis": "^3.1.2", 52 | "istanbul": "^0.4.5", 53 | "mocha": "^2.2.4", 54 | "nanotimer": "^0.3.14", 55 | "nyc": "^15.1.0", 56 | "sinon": "^3.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /benchmark/write.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | 'use strict'; 3 | var Benchmark = require('benchmark'); 4 | var suite = new Benchmark.Suite(); 5 | var Driver = require('../lib/connection.js'); 6 | var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}); 7 | var promises; 8 | var c = 0; 9 | 10 | conn.connect() 11 | .then(function(){ 12 | suite.add('insert', {defer: true, fn: function(defer){ 13 | conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) 14 | .then(function(){defer.resolve();}) 15 | .catch(function(e){ 16 | console.error(e, e.stack); 17 | defer.reject(e); 18 | }); 19 | }}); 20 | 21 | suite.add('insert parallel 50', {defer: true, fn: function(defer){ 22 | try{ 23 | promises = []; 24 | for (let l=0;l<50;l++){ 25 | promises.push(conn.insert('bench', [c++, {user: 'username', data: 'Some data.'}])); 26 | } 27 | var chain = Promise.all(promises); 28 | chain.then(function(){ defer.resolve(); }) 29 | .catch(function(e){ 30 | console.error(e, e.stack); 31 | defer.reject(e); 32 | }); 33 | } catch(e){ 34 | defer.reject(e); 35 | console.error(e, e.stack); 36 | } 37 | }}); 38 | 39 | suite.add('pipelined insert by 10', {defer: true, fn: function(defer){ 40 | var pipelinedConn = conn.pipeline() 41 | 42 | for (var i=0;i<10;i++) { 43 | pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) 44 | } 45 | 46 | pipelinedConn.exec() 47 | .then(function(){ defer.resolve(); }) 48 | .catch(function(e){ defer.reject(e); }) 49 | }}); 50 | 51 | suite.add('pipelined insert by 50', {defer: true, fn: function(defer){ 52 | var pipelinedConn = conn.pipeline() 53 | 54 | for (var i=0;i<50;i++) { 55 | pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}]) 56 | } 57 | 58 | pipelinedConn.exec() 59 | .then(function(){ defer.resolve(); }) 60 | .catch(function(e){ defer.reject(e); }) 61 | }}); 62 | 63 | suite 64 | .on('cycle', function(event) { 65 | console.log(String(event.target)); 66 | }) 67 | .on('complete', function() { 68 | conn.eval('return clear()') 69 | .then(function(){ 70 | console.log('complete'); 71 | process.exit(); 72 | }) 73 | .catch(function(e){ 74 | console.error(e); 75 | }); 76 | }) 77 | .run({ 'async': true, 'queued': true }); 78 | }); -------------------------------------------------------------------------------- /lib/const.js: -------------------------------------------------------------------------------- 1 | var {bufferFrom} = require('./utils') 2 | // i steal it from go 3 | var RequestCode = { 4 | rqConnect: 0x00, //fake for connect 5 | rqSelect: 0x01, 6 | rqInsert: 0x02, 7 | rqReplace: 0x03, 8 | rqUpdate: 0x04, 9 | rqDelete: 0x05, 10 | rqCall: 0x06, 11 | rqAuth: 0x07, 12 | rqEval: 0x08, 13 | rqUpsert: 0x09, 14 | rqCallNew: 0x0a, 15 | rqExecute: 0x0b, 16 | rqDestroy: 0x100, //fake for destroy socket cmd 17 | rqPing: 0x40 18 | }; 19 | 20 | var KeysCode = { 21 | code: 0x00, 22 | sync: 0x01, 23 | schema_version: 0x05, 24 | space_id: 0x10, 25 | index_id: 0x11, 26 | limit: 0x12, 27 | offset: 0x13, 28 | iterator: 0x14, 29 | key: 0x20, 30 | tuple: 0x21, 31 | function_name: 0x22, 32 | username: 0x23, 33 | expression: 0x27, 34 | def_tuple: 0x28, 35 | data: 0x30, 36 | iproto_error_24: 0x31, 37 | meta: 0x32, 38 | sql_text: 0x40, 39 | sql_bind: 0x41, 40 | sql_info: 0x42, 41 | iproto_error: 0x52 42 | }; 43 | 44 | // https://github.com/fl00r/go-tarantool-1.6/issues/2 45 | var IteratorsType = { 46 | eq: 0, 47 | req: 1, 48 | all: 2, 49 | lt: 3, 50 | le: 4, 51 | ge: 5, 52 | gt: 6, 53 | bitsAllSet: 7, 54 | bitsAnySet: 8, 55 | bitsAllNotSet: 9 56 | }; 57 | 58 | var OkCode = 0; 59 | var NetErrCode = 0xfffffff1; // fake code to wrap network problems into response 60 | var TimeoutErrCode = 0xfffffff2; // fake code to wrap timeout error into repsonse 61 | 62 | var PacketLengthBytes = 5; 63 | 64 | var Space = { 65 | schema: 272, 66 | space: 281, 67 | index: 289, 68 | func: 296, 69 | user: 304, 70 | priv: 312, 71 | cluster: 320 72 | }; 73 | 74 | var IndexSpace = { 75 | primary: 0, 76 | name: 2, 77 | indexPrimary: 0, 78 | indexName: 2 79 | }; 80 | 81 | var BufferedIterators = {}; 82 | for(t in IteratorsType) 83 | { 84 | BufferedIterators[t] = bufferFrom([KeysCode.iterator, IteratorsType[t], KeysCode.key]); 85 | } 86 | 87 | var BufferedKeys = {}; 88 | for(k in KeysCode) 89 | { 90 | BufferedKeys[k] = bufferFrom([KeysCode[k]]); 91 | } 92 | 93 | var ExportPackage = { 94 | RequestCode: RequestCode, 95 | KeysCode: KeysCode, 96 | IteratorsType: IteratorsType, 97 | OkCode: OkCode, 98 | passEnter: bufferFrom('a9636861702d73686131', 'hex') /* from msgpack.encode('chap-sha1') */, 99 | Space: Space, 100 | IndexSpace: IndexSpace, 101 | BufferedIterators: BufferedIterators, 102 | BufferedKeys: BufferedKeys 103 | }; 104 | 105 | module.exports = ExportPackage; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // cache the method of Buffer allocation 2 | var createBufferMethod 3 | if (Buffer.allocUnsafe) { 4 | createBufferMethod = Buffer.allocUnsafe; 5 | } else if (Buffer.alloc) { 6 | createBufferMethod = Buffer.alloc; 7 | } else { 8 | createBufferMethod = new Buffer; 9 | } 10 | exports.createBuffer = function (size){ 11 | return createBufferMethod(size); 12 | }; 13 | 14 | // cache the method of Buffer creation using an array of bytes 15 | var bufferFromMethod 16 | if (Buffer.from) { 17 | bufferFromMethod = Buffer.from; 18 | } else { 19 | bufferFromMethod = new Buffer; 20 | } 21 | exports.bufferFrom = function (data, encoding) { 22 | return bufferFromMethod(data, encoding); 23 | } 24 | 25 | // 'buf.slice' is deprecated since Node v17.5.0 / v16.15.0, so we should proceed with 'buf.subarray' 26 | var bufferSubarrayPolyName 27 | if (Buffer.prototype.subarray) { 28 | bufferSubarrayPolyName = 'subarray' 29 | } else { 30 | bufferSubarrayPolyName = 'slice' 31 | }; 32 | exports.bufferSubarrayPoly = bufferSubarrayPolyName; 33 | 34 | exports.findPipelineError = function (array) { 35 | var error = array.find(element => element[0]) 36 | if (error !== undefined) { 37 | return error[0] 38 | } else { 39 | return null; 40 | } 41 | }; 42 | 43 | exports.findPipelineErrors = function (array) { 44 | var array_of_errors = []; 45 | for (var subarray of array) { 46 | var errored_element = subarray[0] 47 | if (errored_element) array_of_errors.push(errored_element) 48 | } 49 | 50 | return array_of_errors 51 | }; 52 | 53 | exports.parseURL = function(str, reserve){ 54 | var result = {}; 55 | if (str.startsWith('/')) { 56 | result.path = str 57 | return result 58 | } 59 | var parsed = str.split(':'); 60 | if(reserve){ 61 | result.username = null; 62 | result.password = null; 63 | } 64 | switch (parsed.length){ 65 | case 1: 66 | result.host = parsed[0]; 67 | break; 68 | case 2: 69 | result.host = parsed[0]; 70 | result.port = parsed[1]; 71 | break; 72 | default: 73 | result.username = parsed[0]; 74 | result.password = parsed[1].split('@')[0]; 75 | result.host = parsed[1].split('@')[1]; 76 | result.port = parsed[2]; 77 | } 78 | return result; 79 | }; 80 | 81 | exports.TarantoolError = function(msg){ 82 | Error.call(this); 83 | this.message = msg; 84 | this.name = 'TarantoolError'; 85 | if (Error.captureStackTrace) { 86 | Error.captureStackTrace(this); 87 | } else { 88 | this.stack = new Error().stack; 89 | } 90 | }; 91 | exports.TarantoolError.prototype = Object.create(Error.prototype); 92 | exports.TarantoolError.prototype.constructor = Error; -------------------------------------------------------------------------------- /lib/sliderBuffer.js: -------------------------------------------------------------------------------- 1 | var { createBuffer } = require('./utils'); 2 | 3 | function SliderBuffer(){ 4 | this.bufferOffset = 0; 5 | this.bufferLength = 0; 6 | this.buffer = createBuffer(1024*10); 7 | } 8 | 9 | SliderBuffer.prototype.add = function(data, from, size){ 10 | var multiplierBuffer = 2; 11 | var newBuffer; 12 | var destLen; 13 | var newLen; 14 | if (typeof from !== 'undefined' && typeof size !== 'undefined') 15 | { 16 | if (this.bufferOffset + this.bufferLength + size < this.buffer.length) 17 | { 18 | data.copy(this.buffer, this.bufferOffset + this.bufferLength, from, from+size); 19 | this.bufferLength+=size; 20 | } 21 | else 22 | { 23 | destLen = size + this.bufferLength; 24 | if (this.buffer.length > destLen) 25 | { 26 | newBuffer = createBuffer(this.buffer.length * multiplierBuffer); 27 | this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); 28 | data.copy(newBuffer, this.bufferLength, from, from+size); 29 | this.buffer = newBuffer; 30 | } 31 | else 32 | { 33 | newLen = this.buffer.length*multiplierBuffer; 34 | while(newLen < destLen) 35 | newLen *= multiplierBuffer; 36 | newBuffer = createBuffer(newLen); 37 | this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); 38 | data.copy(newBuffer, this.bufferLength, from, from+size); 39 | this.buffer = newBuffer; 40 | } 41 | this.bufferOffset = 0; 42 | this.bufferLength = destLen; 43 | } 44 | } 45 | else 46 | { 47 | if (this.bufferOffset + this.bufferLength + data.length < this.buffer.length) 48 | { 49 | data.copy(this.buffer, this.bufferOffset + this.bufferLength); 50 | this.bufferLength+=data.length; 51 | } 52 | else 53 | { 54 | destLen = data.length + this.bufferLength; 55 | if (this.buffer.length > destLen) 56 | { 57 | newBuffer = createBuffer(this.buffer.length * multiplierBuffer); 58 | this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); 59 | data.copy(newBuffer, this.bufferLength); 60 | this.buffer = newBuffer; 61 | } 62 | else 63 | { 64 | newLen = this.buffer.length*multiplierBuffer; 65 | while(newLen < destLen) 66 | newLen *= multiplierBuffer; 67 | newBuffer = createBuffer(newLen); 68 | this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset+this.bufferLength); 69 | data.copy(newBuffer, this.bufferLength); 70 | this.buffer = newBuffer; 71 | } 72 | this.bufferOffset = 0; 73 | this.bufferLength = destLen; 74 | } 75 | } 76 | }; 77 | 78 | module.exports = SliderBuffer; -------------------------------------------------------------------------------- /lib/pipeline.js: -------------------------------------------------------------------------------- 1 | var { 2 | prototype: commandsPrototype 3 | } = require('./commands'); 4 | var {RequestCode} = require('./const'); 5 | 6 | var commandsPrototypeKeys = Object.keys(commandsPrototype); 7 | 8 | function commandInterceptorFactory (method) { 9 | return function commandInterceptor () { 10 | this.pipelinedCommands.push([method, arguments]); 11 | return this 12 | } 13 | } 14 | 15 | var setOfCommandInterceptors = {}; 16 | for (var commandKey of commandsPrototypeKeys) { 17 | setOfCommandInterceptors[commandKey] = commandInterceptorFactory(commandKey) 18 | } 19 | 20 | function sendCommandInterceptorFactory (self) { 21 | return function sendCommandInterceptor (command, buffer, isPipelined) { 22 | // If the Select request was made to search for a system space/index name, then bypass this 23 | if ((RequestCode.rqSelect == command[0]) && !isPipelined) { 24 | self._parent.sendCommand(command, buffer) 25 | return; 26 | } 27 | 28 | self._buffersConcatenedCounter++ 29 | if (self.pipelinedBuffer) { 30 | self.pipelinedBuffer = Buffer.concat([self.pipelinedBuffer, buffer]) 31 | } else { 32 | self.pipelinedBuffer = buffer 33 | } 34 | 35 | self._trySendConcatenedBuffer() 36 | self._parent.sendCommand(command, buffer, true) 37 | } 38 | } 39 | 40 | function _trySendConcatenedBuffer () { 41 | if (this._buffersConcatenedCounter == this.pipelinedCommands.length) { 42 | this._parent.socket.write(this.pipelinedBuffer) 43 | this.flushPipelined() 44 | } 45 | } 46 | 47 | function exec () { 48 | var _this = this; 49 | var promises = []; 50 | for (var interceptedCommand of _this.pipelinedCommands) { 51 | var args = interceptedCommand[1] 52 | if (interceptedCommand[0] == 'select') { 53 | args = Object.values(args) // Otherwise 'arguments' doesn't get applied to function below correctly 54 | args[6] = true // Marks 'select' request as pipelined, otherwise '_getMetadata' will break the pipelined queue 55 | } 56 | promises.push( 57 | new Promise(function (resolve) { 58 | _this._originalMethods[interceptedCommand[0]].apply(_this._originalMethods, args) 59 | .then(function (result) { 60 | resolve([null, result]) 61 | }) 62 | .catch(function (error) { 63 | _this._buffersConcatenedCounter++ // fake, because rejected promise doesn't have its request buffer 64 | _this._trySendConcatenedBuffer() 65 | resolve([error, null]) 66 | }) 67 | }) 68 | ); 69 | } 70 | 71 | return Promise.all(promises) 72 | } 73 | 74 | function flushPipelined () { 75 | this.pipelinedCommands = []; 76 | } 77 | 78 | function Pipeline (self) { 79 | var _this = this; 80 | this._parent = self; 81 | this.pipelinedCommands = []; 82 | this._buffersConcatenedCounter = 0; 83 | this._originalMethods = Object.assign({}, 84 | commandsPrototype, 85 | self, 86 | { 87 | _id: self._id, 88 | sendCommand: sendCommandInterceptorFactory(_this) 89 | } 90 | ) 91 | this.exec = exec 92 | this._trySendConcatenedBuffer = _trySendConcatenedBuffer 93 | this.flushPipelined = flushPipelined 94 | Object.assign(this, setOfCommandInterceptors); 95 | } 96 | 97 | Pipeline.prototype.pipeline = function () { 98 | return new Pipeline(this); 99 | } 100 | 101 | module.exports = Pipeline; -------------------------------------------------------------------------------- /benchmark/read.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | 'use strict'; 3 | 4 | var exec = require('child_process').exec; 5 | var Benchmark = require('benchmark'); 6 | var suite = new Benchmark.Suite(); 7 | var Driver = require('../lib/connection.js'); 8 | var promises; 9 | 10 | var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}); 11 | 12 | conn.connect() 13 | .then(function(){ 14 | suite.add('select cb', {defer: true, fn: function(defer){ 15 | function callback(){ 16 | defer.resolve(); 17 | } 18 | conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error); 19 | }}); 20 | 21 | suite.add('select promise', {defer: true, fn: function(defer){ 22 | conn.select('counter', 0, 1, 0, 'eq', ['test']) 23 | .then(function(){ defer.resolve();}); 24 | }}); 25 | 26 | suite.add('paralell 500', {defer: true, fn: function(defer){ 27 | try{ 28 | promises = []; 29 | for (let l=0;l<500;l++){ 30 | promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test'])); 31 | } 32 | var chain = Promise.all(promises); 33 | chain.then(function(){ defer.resolve(); }) 34 | .catch(function(e){ 35 | console.error(e, e.stack); 36 | defer.reject(e); 37 | }); 38 | } catch(e){ 39 | defer.reject(e); 40 | console.error(e, e.stack); 41 | } 42 | }}); 43 | 44 | suite.add('paralel by 10', {defer: true, fn: function(defer){ 45 | var chain = Promise.resolve(); 46 | try{ 47 | for (var i=0;i<50;i++) 48 | { 49 | chain = chain.then(function(){ 50 | promises = []; 51 | for (var l=0;l<10;l++){ 52 | promises.push( 53 | conn.select('counter', 0, 1, 0, 'eq', ['test']) 54 | ); 55 | } 56 | return Promise.all(promises); 57 | }); 58 | } 59 | 60 | chain.then(function(){ defer.resolve(); }) 61 | .catch(function(e){ 62 | console.error(e, e.stack); 63 | }); 64 | } catch(e){ 65 | console.error(e, e.stack); 66 | } 67 | }}); 68 | 69 | suite.add('paralel by 50', {defer: true, fn: function(defer){ 70 | var chain = Promise.resolve(); 71 | try{ 72 | for (var i=0;i<10;i++) 73 | { 74 | chain = chain.then(function(){ 75 | promises = []; 76 | for (var l=0;l<50;l++){ 77 | promises.push( 78 | conn.select('counter', 0, 1, 0, 'eq', ['test']) 79 | ); 80 | } 81 | return Promise.all(promises); 82 | }); 83 | } 84 | 85 | chain.then(function(){ defer.resolve(); }) 86 | .catch(function(e){ 87 | console.error(e, e.stack); 88 | }); 89 | } catch(e){ 90 | console.error(e, e.stack); 91 | } 92 | }}); 93 | 94 | suite.add('pipelined select by 10', {defer: true, fn: function(defer){ 95 | var pipelinedConn = conn.pipeline() 96 | 97 | for (var i=0;i<10;i++) { 98 | pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); 99 | } 100 | 101 | pipelinedConn.exec() 102 | .then(function(){ defer.resolve(); }) 103 | .catch(function(e){ defer.reject(e); }); 104 | }}); 105 | 106 | suite.add('pipelined select by 50', {defer: true, fn: function(defer){ 107 | var pipelinedConn = conn.pipeline() 108 | 109 | for (var i=0;i<50;i++) { 110 | pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']); 111 | } 112 | 113 | pipelinedConn.exec() 114 | .then(function(){ defer.resolve(); }) 115 | .catch(function(e){ defer.reject(e); }); 116 | }}); 117 | 118 | suite 119 | .on('cycle', function(event) { 120 | console.log(String(event.target)); 121 | }) 122 | .on('complete', function() { 123 | console.log('complete'); 124 | process.exit(); 125 | }) 126 | .run({ 'async': true, 'queued': true }); 127 | }); 128 | -------------------------------------------------------------------------------- /lib/msgpack-extensions.js: -------------------------------------------------------------------------------- 1 | var { createCodec: msgpackCreateCodec} = require('msgpack-lite'); 2 | var { 3 | TarantoolError, 4 | createBuffer, 5 | bufferFrom 6 | } = require('./utils'); 7 | var { 8 | parse: uuidParse, 9 | stringify: uuidStringify 10 | } = require('uuid'); 11 | var { 12 | Uint64BE, 13 | Int64BE 14 | } = require("int64-buffer"); 15 | 16 | var packAs = {} 17 | var codec = msgpackCreateCodec(); 18 | 19 | // Pack big integers correctly (fix for https://github.com/tarantool/node-tarantool-driver/issues/48) 20 | packAs.Integer = function (value) { 21 | if (!Number.isInteger(value)) throw new TarantoolError("Passed value doesn't seems to be an integer") 22 | 23 | if (value > 2147483647) return Uint64BE(value) 24 | if (value < -2147483648) return Int64BE(value) 25 | 26 | return value 27 | } 28 | 29 | // UUID extension 30 | packAs.Uuid = function TarantoolUuidExt (value) { 31 | if (!(this instanceof TarantoolUuidExt)) { 32 | return new TarantoolUuidExt(value) 33 | } 34 | 35 | this.value = value 36 | } 37 | 38 | codec.addExtPacker(0x02, packAs.Uuid, (data) => { 39 | return uuidParse(data.value); 40 | }); 41 | 42 | codec.addExtUnpacker(0x02, (buffer) => { 43 | return uuidStringify(buffer) 44 | }); 45 | 46 | // Decimal extension 47 | function isFloat(n){ 48 | return Number(n) === n && n % 1 !== 0; 49 | } 50 | 51 | packAs.Decimal = function TarantoolDecimalExt (value) { 52 | if (!(this instanceof TarantoolDecimalExt)) { 53 | return new TarantoolDecimalExt(value) 54 | } 55 | 56 | if (!(Number.isInteger(value) || isFloat(value))) { 57 | throw new TarantoolError('Passed value cannot be packed as decimal: expected integer or floating number') 58 | } 59 | 60 | this.value = value 61 | } 62 | 63 | function isOdd (number) { 64 | return number % 2 !== 0; 65 | } 66 | 67 | codec.addExtPacker(0x01, packAs.Decimal, (data) => { 68 | var strNum = data.value.toString() 69 | var rawNum = strNum.replace('-', '') 70 | var scaleBuffer = createBuffer(1) 71 | var rawNumSplitted1 = rawNum.split('.')[1] 72 | scaleBuffer.writeInt8(rawNumSplitted1 && rawNum.split('.')[1].length || 0) 73 | var bufHexed = scaleBuffer.toString('hex') 74 | + rawNum.replace('.', '') 75 | + (strNum.startsWith('-') ? 'b' : 'a') 76 | 77 | if (isOdd(bufHexed.length)) { 78 | bufHexed = bufHexed.slice(0, 2) + '0' + bufHexed.slice(2) 79 | } 80 | 81 | return bufferFrom(bufHexed, 'hex') 82 | }); 83 | 84 | codec.addExtUnpacker(0x01, (buffer) => { 85 | var scale = buffer.readIntBE(0, 1) 86 | var hex = buffer.toString('hex') 87 | 88 | var sign = ['b', 'd'].includes(hex.slice(-1)) ? '-' : '+' 89 | var slicedValue = hex.slice(2).slice(0, -1) 90 | 91 | if (scale > 0) { 92 | var nScale = scale * -1 93 | slicedValue = slicedValue.slice(0, nScale) + '.' + slicedValue.slice(nScale) 94 | } 95 | 96 | return parseFloat(sign + slicedValue) 97 | }); 98 | 99 | // Datetime extension 100 | codec.addExtPacker(0x04, Date, (date) => { 101 | var seconds = date.getTime() / 1000 | 0 102 | var nanoseconds = date.getMilliseconds() * 1000 103 | 104 | var buffer = createBuffer(16) 105 | buffer.writeBigUInt64LE(BigInt(seconds)) 106 | buffer.writeUInt32LE(nanoseconds, 8) 107 | buffer.writeUInt32LE(0, 12) 108 | /* 109 | Node.Js 'Date' doesn't provide nanoseconds, so just using milliseconds. 110 | tzoffset is set to UTC, and tzindex is omitted. 111 | */ 112 | 113 | return buffer; 114 | }); 115 | 116 | codec.addExtUnpacker(0x04, (buffer) => { 117 | var time = new Date(parseInt(buffer.readBigUInt64LE(0)) * 1000) 118 | 119 | if (buffer.length > 8) { 120 | var milliseconds = (buffer.readUInt32LE(8) / 1000 | 0) 121 | time.setMilliseconds(milliseconds) 122 | } 123 | 124 | return time 125 | }) 126 | 127 | exports.packAs = packAs 128 | exports.codec = codec -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var { Decoder: msgpackDecoder } = require('msgpack-lite'); 2 | var tarantoolConstants = require('./const'); 3 | var { TarantoolError } = require('./utils'); 4 | var { codec } = require('./msgpack-extensions'); 5 | 6 | var decoder = new msgpackDecoder({codec}); 7 | 8 | exports._processResponse = function(buffer, offset){ 9 | decoder.buffer = buffer; 10 | decoder.offset = offset || 0; 11 | var headers = decoder.fetch(); 12 | var schemaId = headers[tarantoolConstants.KeysCode.schema_version] 13 | var reqId = headers[tarantoolConstants.KeysCode.sync] 14 | var code = headers[tarantoolConstants.KeysCode.code] 15 | var data = decoder.fetch() 16 | 17 | if (this.schemaId) 18 | { 19 | if (this.schemaId != schemaId) 20 | { 21 | this.schemaId = schemaId; 22 | this.namespace = {}; 23 | } 24 | } 25 | else 26 | { 27 | this.schemaId = schemaId; 28 | } 29 | var task; 30 | for(var i = 0; i 0) { 44 | var command = offlineQueue.shift(); 45 | self.sendCommand( 46 | command[0], 47 | command[1] 48 | ); 49 | } 50 | } 51 | } 52 | 53 | exports.dataHandler = function(self){ 54 | return function(data){ 55 | switch(self.dataState){ 56 | case self.states.PREHELLO: 57 | self.salt = data[bufferSubarrayPoly](64, 108).toString('utf8'); 58 | self.dataState = self.states.CONNECTED; 59 | self.setState(self.states.CONNECTED); 60 | exports.connectHandler(self)(); 61 | break; 62 | case self.states.CONNECTED: 63 | if (data.length >= 5) 64 | { 65 | var len = data.readUInt32BE(1); 66 | var offset = 5; 67 | while(len > 0 && len+offset <= data.length) 68 | { 69 | self._processResponse(data, offset, len); 70 | offset+=len; 71 | if (data.length - offset) 72 | { 73 | if (data.length-offset >= 5) 74 | { 75 | len = data.readUInt32BE(offset+1); 76 | offset+=5; 77 | } 78 | else 79 | { 80 | len = -1; 81 | } 82 | } 83 | else 84 | { 85 | return; 86 | } 87 | } 88 | if (len) 89 | self.awaitingResponseLength = len; 90 | if (self.awaitingResponseLength>0) 91 | self.dataState = self.states.AWAITING; 92 | if (self.awaitingResponseLength<0) 93 | self.dataState = self.states.AWAITING_LENGTH; 94 | self.bufferSlide.add(data, offset, data.length - offset); 95 | } 96 | else 97 | { 98 | self.dataState = self.states.AWAITING_LENGTH; 99 | self.bufferSlide.add(data); 100 | return; 101 | } 102 | break; 103 | case self.states.AWAITING: 104 | self.bufferSlide.add(data); 105 | while(self.awaitingResponseLength > 0 && self.awaitingResponseLength <= self.bufferSlide.bufferLength) 106 | { 107 | self._processResponse(self.bufferSlide.buffer, self.bufferSlide.bufferOffset); 108 | self.bufferSlide.bufferOffset += self.awaitingResponseLength; 109 | self.bufferSlide.bufferLength -= self.awaitingResponseLength; 110 | if (self.bufferSlide.bufferLength) 111 | { 112 | if (self.bufferSlide.bufferLength>=5) 113 | { 114 | self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); 115 | self.bufferSlide.bufferLength-=5; 116 | self.bufferSlide.bufferOffset+=5; 117 | } 118 | else 119 | { 120 | self.awaitingResponseLength = -1; 121 | } 122 | } 123 | else 124 | { 125 | self.awaitingResponseLength = -1; 126 | self.dataState = self.states.CONNECTED; 127 | self.state = self.states.CONNECT; 128 | return; 129 | } 130 | } 131 | if (self.awaitingResponseLength>0) 132 | self.dataState = self.states.AWAITING; 133 | self.state = self.states.AWAITING; 134 | if (self.awaitingResponseLength<0) 135 | self.dataState = self.states.AWAITING_LENGTH; 136 | self.state = self.states.AWAITING_LENGTH; 137 | break; 138 | case self.states.AWAITING_LENGTH: 139 | self.bufferSlide.add(data); 140 | if (self.bufferSlide.bufferLength >= 5) 141 | { 142 | self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); 143 | self.bufferSlide.bufferLength-=5; 144 | self.bufferSlide.bufferOffset+=5; 145 | while(self.awaitingResponseLength >0 && self.awaitingResponseLength <= self.bufferSlide.bufferLength) 146 | { 147 | self._processResponse(self.bufferSlide.buffer, self.bufferSlide.bufferOffset, self.awaitingResponseLength); 148 | self.bufferSlide.bufferOffset += self.awaitingResponseLength; 149 | self.bufferSlide.bufferLength -= self.awaitingResponseLength; 150 | if (self.bufferSlide.bufferLength) 151 | { 152 | if (self.bufferSlide.bufferLength>=5) 153 | { 154 | self.awaitingResponseLength = self.bufferSlide.buffer.readUInt32BE(self.bufferSlide.bufferOffset+1); 155 | self.bufferSlide.bufferLength-=5; 156 | self.bufferSlide.bufferOffset+=5; 157 | } 158 | else 159 | { 160 | self.awaitingResponseLength = -1; 161 | } 162 | } 163 | else 164 | { 165 | self.awaitingResponseLength = -1; 166 | self.dataState = self.states.CONNECTED; 167 | self.state = self.states.CONNECT; 168 | return; 169 | } 170 | } 171 | if (self.awaitingResponseLength>0) 172 | self.dataState = self.states.AWAITING; 173 | if (self.awaitingResponseLength<0) 174 | self.dataState = self.states.AWAITING_LENGTH; 175 | } 176 | break; 177 | } 178 | }; 179 | }; 180 | 181 | exports.errorHandler = function(self){ 182 | return function(error){ 183 | debug('error: %s', error); 184 | self.silentEmit('error', error); 185 | }; 186 | }; 187 | 188 | exports.closeHandler = function (self) { 189 | function close () { 190 | self.setState(self.states.END); 191 | self.flushQueue(new TarantoolError('Connection is closed.')); 192 | } 193 | 194 | return function(){ 195 | process.nextTick(self.emit.bind(self, 'close')); 196 | if (self.manuallyClosing) { 197 | self.manuallyClosing = false; 198 | debug('skip reconnecting since the connection is manually closed.'); 199 | return close(); 200 | } 201 | if (typeof self.options.retryStrategy !== 'function') { 202 | debug('skip reconnecting because `retryStrategy` is not a function'); 203 | return close(); 204 | } 205 | var retryDelay = self.options.retryStrategy(++self.retryAttempts); 206 | 207 | if (typeof retryDelay !== 'number') { 208 | debug('skip reconnecting because `retryStrategy` doesn\'t return a number'); 209 | return close(); 210 | } 211 | self.setState(self.states.RECONNECTING, retryDelay); 212 | if ((self.options.reserveHosts && self.options.reserveHosts.length || 0) > 0) { 213 | if (self.retryAttempts-1 == self.options.beforeReserve){ 214 | self.useNextReserve(); 215 | self.connect().catch(function(){}); 216 | return; 217 | } 218 | } 219 | debug('reconnect in %sms', retryDelay); 220 | 221 | self.reconnectTimeout = setTimeout(function () { 222 | self.reconnectTimeout = null; 223 | self.connect().catch(function(){}); 224 | }, retryDelay); 225 | }; 226 | }; -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var debug = require('debug')('tarantool-driver:main'); 5 | var _ = require('lodash'); 6 | var { 7 | parseURL, 8 | TarantoolError, 9 | findPipelineError, 10 | findPipelineErrors 11 | } = require('./utils'); 12 | var { packAs } = require('./msgpack-extensions'); 13 | var Denque = require('denque'); 14 | var tarantoolConstants = require('./const'); 15 | var Commands = require('./commands'); 16 | var Pipeline = require('./pipeline'); 17 | var Connector = require('./connector'); 18 | var eventHandler = require('./event-handler'); 19 | var SliderBuffer = require('./sliderBuffer') 20 | 21 | var revertStates = { 22 | 0: 'connecting', 23 | 1: 'connected', 24 | 2: 'awaiting', 25 | 4: 'inited', 26 | 8: 'prehello', 27 | 16: 'awaiting_length', 28 | 32: 'end', 29 | 64: 'reconnecting', 30 | 128: 'auth', 31 | 256: 'connect', 32 | 512: 'changing_host' 33 | }; 34 | TarantoolConnection.defaultOptions = { 35 | host: 'localhost', 36 | port: 3301, 37 | path: null, 38 | username: null, 39 | password: null, 40 | reserveHosts: [], 41 | beforeReserve: 2, 42 | timeout: 0, 43 | noDelay: true, 44 | keepAlive: true, 45 | nonWritableHostPolicy: null, /* What to do when Tarantool server rejects write operation, 46 | e.g. because of box.cfg.read_only set to 'true' or during fetching snapshot. 47 | Possible values are: 48 | - null: reject Promise 49 | - 'changeHost': disconnect from the current host and connect to the next of 'reserveHosts'. Pending Promise will be rejected. 50 | - 'changeAndRetry': same as 'changeHost', but after connecting tries to run the command again in order to fullfil the Promise 51 | */ 52 | maxRetriesPerRequest: 5, // If 'nonWritableHostPolicy' specified, Promise will be rejected only after exceeding this setting 53 | enableOfflineQueue: true, 54 | retryStrategy: function (times) { 55 | return Math.min(times * 50, 2000); 56 | }, 57 | lazyConnect: false 58 | }; 59 | 60 | function TarantoolConnection (){ 61 | if (!(this instanceof TarantoolConnection)) { 62 | return new TarantoolConnection(arguments[0], arguments[1], arguments[2]); 63 | } 64 | EventEmitter.call(this); 65 | this.reserve = []; 66 | this.parseOptions(arguments[0], arguments[1], arguments[2]); 67 | this.connector = new Connector(this.options); 68 | this.schemaId = null; 69 | this.states = { 70 | CONNECTING: 0, 71 | CONNECTED: 1, 72 | AWAITING: 2, 73 | INITED: 4, 74 | PREHELLO: 8, 75 | AWAITING_LENGTH: 16, 76 | END: 32, 77 | RECONNECTING: 64, 78 | AUTH: 128, 79 | CONNECT: 256, 80 | CHANGING_HOST: 512 81 | }; 82 | this.dataState = this.states.PREHELLO; 83 | this.commandsQueue = new Denque(); 84 | this.offlineQueue = new Denque(); 85 | this.namespace = {}; 86 | this.bufferSlide = new SliderBuffer() 87 | this.awaitingResponseLength = -1; 88 | this.retryAttempts = 0; 89 | this._id = 0; 90 | this.findPipelineError = findPipelineError 91 | this.findPipelineErrors = findPipelineErrors 92 | if (this.options.lazyConnect) { 93 | this.setState(this.states.INITED); 94 | } else { 95 | this.connect().catch(_.noop); 96 | } 97 | } 98 | 99 | util.inherits(TarantoolConnection, EventEmitter); 100 | _.assign(TarantoolConnection.prototype, Commands.prototype); 101 | _.assign(TarantoolConnection.prototype, Pipeline.prototype); 102 | _.assign(TarantoolConnection.prototype, require('./parser')); 103 | 104 | for (var packerName of Object.keys(packAs)) { 105 | TarantoolConnection.prototype['pack' + packerName] = packAs[packerName] 106 | } 107 | 108 | TarantoolConnection.prototype.resetOfflineQueue = function () { 109 | this.offlineQueue = new Denque(); 110 | }; 111 | 112 | TarantoolConnection.prototype.parseOptions = function(){ 113 | this.options = {}; 114 | var i; 115 | for (i = 0; i < arguments.length; ++i) { 116 | var arg = arguments[i]; 117 | if (arg === null || typeof arg === 'undefined') { 118 | continue; 119 | } 120 | if (typeof arg === 'object') { 121 | _.defaults(this.options, arg); 122 | } else if (typeof arg === 'string') { 123 | if(!isNaN(arg) && (parseFloat(arg) | 0) === parseFloat(arg)){ 124 | this.options.port = arg; 125 | continue; 126 | } 127 | _.defaults(this.options, parseURL(arg)); 128 | } else if (typeof arg === 'number') { 129 | this.options.port = arg; 130 | } else { 131 | throw new TarantoolError('Invalid argument ' + arg); 132 | } 133 | } 134 | _.defaults(this.options, TarantoolConnection.defaultOptions); 135 | var reserveHostsLength = this.options.reserveHosts && this.options.reserveHosts.length || 0 136 | if ((this.options.nonWritableHostPolicy != null) && (reserveHostsLength == 0)) { 137 | throw new TarantoolError('\'nonWritableHostPolicy\' option is specified, but there are no reserve hosts. Specify it in connection options via \'reserveHosts\'') 138 | } 139 | if (typeof this.options.port === 'string') { 140 | this.options.port = parseInt(this.options.port, 10); 141 | } 142 | if (this.options.path != null) { 143 | delete this.options.port 144 | delete this.options.host 145 | } 146 | if (reserveHostsLength > 0){ 147 | this.reserveIterator = 1; 148 | this.reserve.push(_.pick(this.options, ['port', 'host', 'username', 'password', 'path'])); 149 | for(i = 0; i %s(%s)', command[0], command[1]); 180 | this.offlineQueue.push([command, buffer]); 181 | } else { 182 | if (this.options.nonWritableHostPolicy == 'changeAndRetry') { 183 | command.push(buffer) 184 | } 185 | this.commandsQueue.push(command); 186 | if (!isPipelined) this.socket.write(buffer); // in pipelined mode data is written via its own function 187 | } 188 | break; 189 | case this.states.END: 190 | command[2].reject(new TarantoolError('Connection is closed.')); 191 | break; 192 | default: 193 | debug('queue -> %s(%s)', command[0], command[1]); 194 | if (!this.options.enableOfflineQueue) { 195 | return command[2].reject(new TarantoolError('Connection not established yet!')); 196 | } 197 | this.offlineQueue.push([command, buffer]); 198 | } 199 | }; 200 | 201 | TarantoolConnection.prototype.setState = function (state, arg) { 202 | var address; 203 | if (this.socket && this.socket.remoteAddress && this.socket.remotePort) { 204 | address = this.socket.remoteAddress + ':' + this.socket.remotePort; 205 | } else { 206 | if (this.options.path != null) { 207 | address = this.options.path 208 | } else { 209 | address = this.options.host + ':' + this.options.port; 210 | } 211 | } 212 | debug('state[%s]: %s -> %s', address, revertStates[this.state] || '[empty]', revertStates[state]); 213 | this.state = state; 214 | process.nextTick(this.emit.bind(this, revertStates[state], arg)); 215 | }; 216 | 217 | TarantoolConnection.prototype.connect = function(){ 218 | return new Promise(function (resolve, reject) { 219 | if (this.state === this.states.CONNECTING || this.state === this.states.CONNECT || this.state === this.states.CONNECTED || this.state === this.states.AUTH) { 220 | reject(new TarantoolError('Tarantool is already connecting/connected')); 221 | return; 222 | } 223 | this.setState(this.states.CONNECTING); 224 | var _this = this; 225 | this.connector.connect(function(err, socket){ 226 | if(err){ 227 | _this.flushQueue(err); 228 | _this.silentEmit('error', err); 229 | reject(err); 230 | _this.setState(_this.states.END); 231 | return; 232 | } 233 | _this.socket = socket; 234 | socket.once('connect', eventHandler.connectHandler(_this)); 235 | socket.once('error', eventHandler.errorHandler(_this)); 236 | socket.once('close', eventHandler.closeHandler(_this)); 237 | socket.on('data', eventHandler.dataHandler(_this)); 238 | 239 | if (_this.options.timeout) { 240 | socket.setTimeout(_this.options.timeout, function () { 241 | socket.setTimeout(0); 242 | socket.destroy(); 243 | 244 | var error = new TarantoolError('connect ETIMEDOUT'); 245 | error.errorno = 'ETIMEDOUT'; 246 | error.code = 'ETIMEDOUT'; 247 | error.syscall = 'connect'; 248 | eventHandler.errorHandler(_this)(error); 249 | }); 250 | socket.once('connect', function () { 251 | socket.setTimeout(0); 252 | }); 253 | } 254 | var connectionConnectHandler = function () { 255 | _this.removeListener('close', connectionCloseHandler); 256 | resolve(); 257 | }; 258 | var connectionCloseHandler = function () { 259 | _this.removeListener('connect', connectionConnectHandler); 260 | reject(new Error('Connection is closed.')); 261 | }; 262 | _this.once('connect', connectionConnectHandler); 263 | _this.once('close', connectionCloseHandler); 264 | }); 265 | }.bind(this)); 266 | }; 267 | 268 | TarantoolConnection.prototype.flushQueue = function (error) { 269 | while (this.offlineQueue.length > 0) { 270 | this.offlineQueue.shift()[0][2].reject(error); 271 | } 272 | while (this.commandsQueue.length > 0) { 273 | this.commandsQueue.shift()[2].reject(error); 274 | } 275 | }; 276 | 277 | TarantoolConnection.prototype.silentEmit = function (eventName) { 278 | var error; 279 | if (eventName === 'error') { 280 | error = arguments[1]; 281 | 282 | if (this.status === 'end') { 283 | return; 284 | } 285 | 286 | if (this.manuallyClosing) { 287 | if ( 288 | error instanceof Error && 289 | ( 290 | error.message === 'Connection manually closed' || 291 | error.syscall === 'connect' || 292 | error.syscall === 'read' 293 | ) 294 | ) { 295 | return; 296 | } 297 | } 298 | } 299 | if (this.listeners(eventName).length > 0) { 300 | return this.emit.apply(this, arguments); 301 | } 302 | if (error && error instanceof Error) { 303 | console.error('[tarantool-driver] Unhandled error event:', error.stack); 304 | } 305 | return false; 306 | }; 307 | TarantoolConnection.prototype.destroy = function () { 308 | this.disconnect(); 309 | }; 310 | TarantoolConnection.prototype.disconnect = function(reconnect){ 311 | if (!reconnect) { 312 | this.manuallyClosing = true; 313 | } 314 | if (this.reconnectTimeout) { 315 | clearTimeout(this.reconnectTimeout); 316 | this.reconnectTimeout = null; 317 | } 318 | if (this.state === this.states.INITED) { 319 | eventHandler.closeHandler(this)(); 320 | } else { 321 | this.connector.disconnect(); 322 | } 323 | }; 324 | 325 | TarantoolConnection.prototype.IteratorsType = tarantoolConstants.IteratorsType; 326 | 327 | module.exports = TarantoolConnection; 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js driver for tarantool 1.7+ 2 | 3 | [![Build Status](https://travis-ci.org/tarantool/node-tarantool-driver.svg)](https://travis-ci.org/tarantool/node-tarantool-driver) 4 | 5 | Node tarantool driver for 1.7+ support Node.js v.4+. 6 | 7 | Based on [go-tarantool](https://github.com/tarantool/go-tarantool) and implements [Tarantool’s binary protocol](http://tarantool.org/doc/dev_guide/box-protocol.html), for more information you can read them or basic documentation at [Tarantool manual](http://tarantool.org/doc/). 8 | 9 | Code architecture and some features in version 3 borrowed from the [ioredis](https://github.com/luin/ioredis). 10 | 11 | [msgpack-lite](https://github.com/kawanet/msgpack-lite) package used as MsgPack encoder/decoder. 12 | 13 | 14 | 15 | 16 | ## Table of contents 17 | 18 | * [Installation](#installation) 19 | * [Configuration](#configuration) 20 | * [Usage example](#usage-example) 21 | * [Msgpack implementation](#msgpack-implementation) 22 | * [API reference](#api-reference) 23 | * [Debugging](#debugging) 24 | * [Contributions](#contributions) 25 | * [Changelog](#changelog) 26 | 27 | ## Installation 28 | 29 | ```Bash 30 | npm install --save tarantool-driver 31 | ``` 32 | ## Configuration 33 | 34 | new Tarantool([port], [host], [options]) ⇐ [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) 35 | 36 | Creates a Tarantool instance, extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter). 37 | 38 | Connection related custom events: 39 | * "reconnecting" - emitted when the client try to reconnect, first argument is retry delay in ms. 40 | * "connect" - emitted when the client connected and auth passed (if username and password provided), first argument is an object with host and port of the Taranool server. 41 | * "change_host" - emitted when `nonWritableHostPolicy` option is set and write error occurs, first argument is the text of error which provoked the host to be changed. 42 | 43 | | Param | Type | Default | Description | 44 | | --- | --- | --- | --- | 45 | | [port] | number \| string \| Object | 3301 | Port of the Tarantool server, or a URI string (see the examples in [tarantool configuration doc](https://tarantool.org/en/doc/reference/configuration/index.html#uri)), or the `options` object(see the third argument). | 46 | | [host] | string \| Object | "localhost" | Host of the Tarantool server, when the first argument is a URL string, this argument is an object represents the options. | 47 | | [path] | string \| Object | null | Unix socket path of the Tarantool server. | 48 | | [options] | Object | | Other options, including all from [net.createConnection](https://nodejs.org/api/net.html#netcreateconnection). | 49 | | [options.port] | number | 6379 | Port of the Tarantool server. | 50 | | [options.host] | string | "localhost" | Host of the Tarantool server. | 51 | | [options.username] | string | null | If set, client will authenticate with the value of this option when connected. | 52 | | [options.password] | string | null | If set, client will authenticate with the value of this option when connected. | 53 | | [options.timeout] | number | 0 | The milliseconds before a timeout occurs during the initial connection to the Tarantool server. | 54 | | [options.tls] | Object | null | If specified, forces to use `tls` module instead of the default `net`. In object properties you can specify any TLS-related options, e.g. from the [tls.createSecureContext()](https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions) | 55 | | [options.keepAlive] | boolean | true | Enables keep-alive functionality (recommended). | 56 | | [options.noDelay] | boolean | true | Disables the use of Nagle's algorithm (recommended). | 57 | | [options.lazyConnect] | boolean | false | By default, When a new `Tarantool` instance is created, it will connect to Tarantool server automatically. If you want to keep disconnected util a command is called, you can pass the `lazyConnect` option to the constructor. | 58 | | [options.nonWritableHostPolicy] | string | null | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching.
Possible values are:
- `null`: just rejects Promise with an error
- `changeHost`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected.
- `changeAndRetry`: same as `changeHost`, but after reconnecting tries to run the command again in order to fullfil the Promise | 59 | | [options.maxRetriesPerRequest] | number | 5 | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. | 60 | | [options.enableOfflineQueue] | boolean | true | By default, if there is no active connection to the Tarantool server, commands are added to a queue and are executed once the connection is "ready", meaning the connection to the Tarantool server has been established and auth passed (`connect` event is also executed at this moment). If this option is false, when execute the command when the connection isn't ready, an error will be returned. | 61 | | [options.reserveHosts] | array | [] | Array of [strings](https://tarantool.org/en/doc/reference/configuration/index.html?highlight=uri#uri) - reserve hosts. Client will try to connect to hosts from this array after loosing connection with current host and will do it cyclically. See example below.| 62 | | [options.beforeReserve] | number | 2 | Number of attempts to reconnect before connect to next host from the reserveHosts | 63 | | [options.retryStrategy] | function | | See below | 64 | 65 | ### Reserve hosts example: 66 | 67 | ```javascript 68 | let connection = new Tarantool({ 69 | host: 'mail.ru', 70 | port: 33013, 71 | username: 'user' 72 | password: 'secret', 73 | reserveHosts: [ 74 | 'anotheruser:difficultpass@mail.ru:33033', 75 | '127.0.0.1:3301' 76 | ], 77 | beforeReserve: 1 78 | }) 79 | // connect to mail.ru:33013 -> dead 80 | // ↓ 81 | // trying connect to mail.ru:33033 -> dead 82 | // ↓ 83 | // trying connect to 127.0.0.1:3301 -> dead 84 | // ↓ 85 | // trying connect to mail.ru:33013 ...etc 86 | ``` 87 | 88 | ### Retry strategy 89 | 90 | By default, node-tarantool-driver client will try to reconnect when the connection to Tarantool is lost 91 | except when the connection is closed manually by `tarantool.disconnect()`. 92 | 93 | It's very flexible to control how long to wait to reconnect after disconnection 94 | using the `retryStrategy` option: 95 | 96 | ```javascript 97 | var tarantool = new Tarantool({ 98 | // This is the default value of `retryStrategy` 99 | retryStrategy: function (times) { 100 | var delay = Math.min(times * 50, 2000); 101 | return delay; 102 | } 103 | }); 104 | ``` 105 | 106 | 107 | `retryStrategy` is a function that will be called when the connection is lost. 108 | The argument `times` means this is the nth reconnection being made and 109 | the return value represents how long (in ms) to wait to reconnect. When the 110 | return value isn't a number, node-tarantool-driver will stop trying to reconnect, and the connection 111 | will be lost forever if the user doesn't call `tarantool.connect()` manually. 112 | 113 | **This feature is borrowed from the [ioredis](https://github.com/luin/ioredis)** 114 | 115 | ## Usage example 116 | 117 | We use TarantoolConnection instance and connect before other operations. Methods call return promise(https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Promise). Available methods with some testing: select, update, replace, insert, delete, auth, destroy. 118 | ```javascript 119 | var TarantoolConnection = require('tarantool-driver'); 120 | var conn = new TarantoolConnection('notguest:sesame@mail.ru:3301'); 121 | 122 | // select arguments space_id, index_id, limit, offset, iterator, key 123 | conn.select(512, 0, 1, 0, 'eq', [50]) 124 | .then(funtion(results){ 125 | doSomeThingWithResults(results); 126 | }); 127 | ``` 128 | 129 | 130 | ## Msgpack implementation 131 | 132 | You can use any implementation that can be duck typing with next interface: 133 | 134 | ```Javascript 135 | //msgpack implementation example 136 | /* 137 | @interface 138 | decode: (Buffer buf) 139 | encode: (Object obj) 140 | */ 141 | var exampleCustomMsgpack = { 142 | encode: function(obj){ 143 | return yourmsgpack.encode(obj); 144 | }, 145 | decode: function(buf){ 146 | return yourmsgpack.decode(obj); 147 | } 148 | }; 149 | ``` 150 | 151 | By default use msgpack-lite package. 152 | 153 | ## API reference 154 | 155 | ### tarantool.connect() ⇒ Promise 156 | 157 | Resolve if connected. Or reject if not. 158 | 159 | ### tarantool._auth(login: String, password: String) ⇒ Promise 160 | 161 | **An internal method. The connection should be established before invoking.** 162 | 163 | Auth with using [chap-sha1](http://tarantool.org/doc/book/box/box_space.html). About authenthication more here: [authentication](http://tarantool.org/doc/book/box/authentication.html) 164 | 165 | ### tarantool.packUuid(uuid: String) 166 | 167 | **Method for converting [UUID values](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#uuid) to Tarantool-compatible format.** 168 | 169 | If passing UUID without converion via this method, server will accept it as simple String. 170 | 171 | ### tarantool.packDecimal(numberToConvert: Number) 172 | 173 | **Method for converting Numbers (Float or Integer) to Tarantool [Decimal](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#decimal) type.** 174 | 175 | If passing number without converion via this method, server will accept it as Integer or Double (for JS Float type). 176 | 177 | ### tarantool.packInteger(numberToConvert: Number) 178 | 179 | **Method for safely passing numbers up to int64 to bind params** 180 | 181 | Otherwise msgpack will encode anything bigger than int32 as a double number. 182 | 183 | ### tarantool.select(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple) ⇒ Promise 184 | 185 | [Iterators](http://tarantool.org/doc/book/box/box_index.html). Available iterators: 'eq', 'req', 'all', 'lt', 'le', 'ge', 'gt', 'bitsAllSet', 'bitsAnySet', 'bitsAllNotSet'. 186 | 187 | It's just select. Promise resolve array of tuples. 188 | 189 | Some examples: 190 | 191 | ```Javascript 192 | conn.select(512, 0, 1, 0, 'eq', [50]); 193 | //same as 194 | conn.select('test', 'primary', 1, 0, 'eq', [50]); 195 | ``` 196 | 197 | You can use space name or index name instead of id, but this way some requests will be made to get and cache metadata. This stored information will be actual for delete, replace, insert, update too. 198 | 199 | For tests, we will create a Space named 'users' on the Tarantool server-side, where the 'id' index is of UUID type: 200 | 201 | ```lua 202 | -- example schema of such space 203 | box.schema.space.create("users", {engine = 'memtx'}) 204 | box.space.users:format({ 205 | {name = 'id', type = 'uuid', is_nullable = false}, 206 | {name = 'username', type = 'string', is_nullable = false} 207 | }) 208 | ``` 209 | And then select some tuples on a client side: 210 | ```Javascript 211 | conn.select('users', 'id', 1, 0, 'eq', [conn.packUuid('550e8400-e29b-41d4-a716-446655440000')]); 212 | ``` 213 | 214 | ### tarantool.selectCb(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple, callback: function(success), callback: function(error)) 215 | 216 | Same as [tarantool.select](#select) but with callbacks. 217 | 218 | ### tarantool.delete(spaceId: Number or String, indexId: Number or String, key: tuple) ⇒ Promise 219 | 220 | Promise resolve an array of deleted tuples. 221 | 222 | ### tarantool.update(spaceId: Number or String, indexId: Number or String, key: tuple, ops) ⇒ Promise 223 | 224 | [Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update) 225 | 226 | Promise resolve an array of updated tuples. 227 | 228 | ### tarantool.insert(spaceId: Number or String, tuple) ⇒ Promise 229 | 230 | More you can read here: [Insert](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.insert) 231 | 232 | Promise resolve a new tuple. 233 | 234 | ### tarantool.upsert(spaceId: Number or String, ops: array of operations, tuple: tuple) ⇒ Promise 235 | 236 | About operation: [Upsert](http://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.upsert) 237 | 238 | [Possible operators.](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.update) 239 | 240 | Promise resolve nothing. 241 | 242 | ### tarantool.replace(spaceId: Number or String, tuple: tuple) ⇒ Promise 243 | 244 | More you can read here: [Replace](https://tarantool.org/doc/book/box/box_space.html#lua-function.space_object.replace) 245 | 246 | Promise resolve a new or replaced tuple. 247 | 248 | ### tarantool.call(functionName: String, args...) ⇒ Promise 249 | 250 | Call a function with arguments. 251 | 252 | You can create function on tarantool side: 253 | ```Lua 254 | function myget(id) 255 | val = box.space.batched:select{id} 256 | return val[1] 257 | end 258 | ``` 259 | 260 | And then use something like this: 261 | ```Javascript 262 | conn.call('myget', 4) 263 | .then(function(value){ 264 | console.log(value); 265 | }); 266 | ``` 267 | 268 | If you have a 2 arguments function just send a second arguments in this way: 269 | ```Javascript 270 | conn.call('my2argumentsfunc', 'first', 'second argument') 271 | ``` 272 | And etc like this. 273 | 274 | Because lua support a multiple return it's always return array or undefined. 275 | 276 | ### tarantool.eval(expression: String) ⇒ Promise 277 | 278 | Evaluate and execute the expression in Lua-string. [Eval](https://tarantool.org/doc/reference/reference_lua/net_box.html?highlight=eval#lua-function.conn.eval) 279 | 280 | Promise resolve result:any. 281 | 282 | Example: 283 | 284 | 285 | ```Javascript 286 | conn.eval('return box.session.user()') 287 | .then(function(res){ 288 | console.log('current user is:' res[0]) 289 | }) 290 | ``` 291 | 292 | ### tarantool.sql(query: String, bindParams: Array) ⇒ Promise 293 | 294 | It's accessible only in 2.1 tarantool. 295 | 296 | You can use SQL query that is like sqlite dialect to query a tarantool database. 297 | 298 | You can insert or select or create database. 299 | 300 | More about it [here](https://www.tarantool.io/en/doc/2.1/tutorials/sql_tutorial/). 301 | 302 | Example: 303 | 304 | ```Javascript 305 | await connection.insert('tags', ['tag_1', 1]) 306 | await connection.insert('tags', ['tag_2', 50]) 307 | connection.sql('select * from "tags"') 308 | .then((res) => { 309 | console.log('Successful get tags', res); 310 | }) 311 | .catch((error) => { 312 | console.log(error); 313 | }); 314 | ``` 315 | 316 | P.S. If you using lowercase in your space name you need to use a double quote for their name. 317 | 318 | It doesn't work for space without format. 319 | 320 | ### tarantool.pipeline().<...>.exec() 321 | 322 | Queue some commands in memory and then send them simultaneously to the server in a single (or several, if request body is too big) network call(s). 323 | This way the performance is significantly improved by more than 300% (depending on the number of pipelined commands - the bigger, the better) 324 | 325 | Example: 326 | 327 | ```Javascript 328 | tarantool.pipeline() 329 | .insert('tags', ['tag_1', 1]) 330 | .insert('tags', ['tag_2', 50]) 331 | .sql('update "tags" set "amount" = 10 where "tag_id" = \'tag_1\'') 332 | .update('tags', 'tag_id', ['tag_2'], [['=', 'amount', 30]]) 333 | .sql('select * from "tags"') 334 | .call('truncateTags') 335 | .exec() 336 | ``` 337 | 338 | ### tarantool.ping() ⇒ Promise 339 | 340 | Promise resolve true. 341 | 342 | ### ~~tarantool.destroy(interupt: Boolean) ⇒ Promise~~ 343 | ***Deprecated*** 344 | ### tarantool.disconnect() 345 | Disconnect from Tarantool. 346 | 347 | This method closes the connection immediately, 348 | and may lose some pending replies that haven't written to client. 349 | 350 | ## Debugging 351 | 352 | Set environment variable "DEBUG" to "tarantool-driver:*" 353 | 354 | ## Contributions 355 | 356 | It's ok you can do whatever you need. I add log options for some technical information it can be help for you. If i don't answer i just miss email :( it's a lot emails from github so please write me to newbiecraft@gmail.com directly if i don't answer in one day. 357 | 358 | ## Changelog 359 | 360 | ### 3.1.0 361 | 362 | - Added 3 new msgpack extensions: UUID, Datetime, Decimal. 363 | - Connection object now accepts all options of `net.createConnection()`, including Unix socket path. 364 | - New `nonWritableHostPolicy` and related options, which improves a high availability capabilities without any 3rd parties. 365 | - Ability to disable the offline queue. 366 | - Fixed [bug with int32](https://github.com/tarantool/node-tarantool-driver/issues/48) numbers when it was encoded as floating. Use method `packInteger()` to solve this. 367 | - `selectCb()` now also accepts `spaceId` and `indexId` as their String names, not only their IDs. 368 | - Some performance improvements by caching internal values. 369 | - TLS (SSL) support. 370 | - New `pipeline()`+`exec()` methods kindly borrowed from the [ioredis](https://github.com/redis/ioredis?tab=readme-ov-file#pipelining), which lets you to queue some commands in memory and then send them simultaneously to the server in a single (or several, if request body is too big) network call(s). Thanks to the Tarantool, which [made this possible](https://www.tarantool.io/en/doc/latest/dev_guide/internals/iproto/format/#packet-structure). 371 | This way the performance is significantly improved by 500-1600% - you can check it yourself by running `npm run benchmark-read` or `npm run benchmark-write`. 372 | Note that this feature doesn't replaces the Transaction model, which has some level of isolation. 373 | - Changed `const` declaration to `var` in order to support old Node.JS versions. 374 | 375 | ### 3.0.7 376 | 377 | Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions. 378 | 379 | ### 3.0.6 380 | 381 | Remove let for support old nodejs version 382 | 383 | ### 3.0.5 384 | 385 | Add support SQL 386 | 387 | ### 3.0.4 388 | 389 | Fix eval and call 390 | 391 | ### 3.0.3 392 | 393 | Increase request id limit to SMI Maximum 394 | 395 | ### 3.0.2 396 | 397 | Fix parser thx @tommiv 398 | 399 | ### 3.0.0 400 | 401 | New version with reconnect in alpha. 402 | 403 | ### 1.0.0 404 | 405 | Fix test for call changes and remove unuse upsert parameter (critical change API for upsert) 406 | 407 | ### 0.4.1 408 | 409 | Add clear schema cache on change schema id 410 | 411 | ### 0.4.0 412 | 413 | Change msgpack5 to msgpack-lite(thx to @arusakov). 414 | Add msgpack as option for connection. 415 | Bump msgpack5 for work at new version. 416 | 417 | ### 0.3.0 418 | Add upsert operation. 419 | Key is now can be just a number. 420 | 421 | ## ToDo 422 | 423 | 1. Streams 424 | 2. Events and subscriptions 425 | 3. Graceful shutdown protocol 426 | 4. Prepared SQL statements -------------------------------------------------------------------------------- /lib/commands.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | var { createHash } = require('crypto'); 3 | var tarantoolConstants = require('./const'); 4 | var { 5 | bufferFrom, 6 | createBuffer, 7 | TarantoolError, 8 | bufferSubarrayPoly 9 | } = require('./utils'); 10 | var { encode: msgpackEncode } = require('msgpack-lite'); 11 | var { codec } = require('./msgpack-extensions'); 12 | 13 | function Commands() {} 14 | Commands.prototype.sendCommand = function () {}; 15 | 16 | var maxSmi = 1<<30 17 | 18 | Commands.prototype._getRequestId = function(){ 19 | if (this._id > maxSmi) 20 | this._id =0; 21 | return this._id++; 22 | }; 23 | 24 | Commands.prototype._getSpaceId = function(name){ 25 | var _this = this; 26 | return this.select(tarantoolConstants.Space.space, tarantoolConstants.IndexSpace.name, 1, 0, 27 | 'eq', [name]) 28 | .then(function(value){ 29 | if (value && value.length && value[0]) 30 | { 31 | var spaceId = value[0][0]; 32 | _this.namespace[name] = { 33 | id: spaceId, 34 | name: name, 35 | indexes: {} 36 | }; 37 | _this.namespace[spaceId] = { 38 | id: spaceId, 39 | name: name, 40 | indexes: {} 41 | }; 42 | return spaceId; 43 | } 44 | else 45 | { 46 | throw new TarantoolError('Cannot read a space name or space is not defined'); 47 | } 48 | }); 49 | }; 50 | Commands.prototype._getIndexId = function(spaceId, indexName){ 51 | var _this = this; 52 | return this.select(tarantoolConstants.Space.index, tarantoolConstants.IndexSpace.indexName, 1, 0, 53 | 'eq', [spaceId, indexName]) 54 | .then(function(value) { 55 | if (value && value[0] && value[0].length>1) { 56 | var indexId = value[0][1]; 57 | var space = _this.namespace[spaceId]; 58 | if (space) { 59 | _this.namespace[space.name].indexes[indexName] = indexId; 60 | _this.namespace[space.id].indexes[indexName] = indexId; 61 | } 62 | return indexId; 63 | } 64 | else 65 | throw new TarantoolError('Cannot read a space name indexes or index is not defined'); 66 | }); 67 | }; 68 | Commands.prototype.select = function(spaceId, indexId, limit, offset, iterator, key, _isPipelined) { 69 | var _this = this; 70 | if (!(key instanceof Array)) 71 | key = [key]; 72 | return new Promise(function(resolve, reject){ 73 | if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) 74 | spaceId = _this.namespace[spaceId].id; 75 | if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) 76 | indexId = _this.namespace[spaceId].indexes[indexId]; 77 | if (typeof(spaceId)=='string' || typeof(indexId)=='string') 78 | { 79 | return _this._getMetadata(spaceId, indexId) 80 | .then(function(info){ 81 | return _this.select(info[0], info[1], limit, offset, iterator, key, _isPipelined); 82 | }) 83 | .then(resolve) 84 | .catch(reject); 85 | } 86 | var reqId = _this._getRequestId(); 87 | 88 | if (iterator == 'all') 89 | key = []; 90 | var bufKey = msgpackEncode(key, {codec}); 91 | var len = 31+bufKey.length; 92 | var buffer = createBuffer(5+len); 93 | 94 | buffer[0] = 0xce; 95 | buffer.writeUInt32BE(len, 1); 96 | buffer[5] = 0x82; 97 | buffer[6] = tarantoolConstants.KeysCode.code; 98 | buffer[7] = tarantoolConstants.RequestCode.rqSelect; 99 | buffer[8] = tarantoolConstants.KeysCode.sync; 100 | buffer[9] = 0xce; 101 | buffer.writeUInt32BE(reqId, 10); 102 | buffer[14] = 0x86; 103 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 104 | buffer[16] = 0xcd; 105 | buffer.writeUInt16BE(spaceId, 17); 106 | buffer[19] = tarantoolConstants.KeysCode.index_id; 107 | buffer.writeUInt8(indexId, 20); 108 | buffer[21] = tarantoolConstants.KeysCode.limit; 109 | buffer[22] = 0xce; 110 | buffer.writeUInt32BE(limit, 23); 111 | buffer[27] = tarantoolConstants.KeysCode.offset; 112 | buffer[28] = 0xce; 113 | buffer.writeUInt32BE(offset, 29); 114 | buffer[33] = tarantoolConstants.KeysCode.iterator; 115 | buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 34); 116 | buffer[35] = tarantoolConstants.KeysCode.key; 117 | bufKey.copy(buffer, 36); 118 | 119 | _this.sendCommand([ 120 | tarantoolConstants.RequestCode.rqSelect, 121 | reqId, 122 | {resolve: resolve, reject: reject} 123 | ], 124 | buffer, 125 | _isPipelined === true 126 | ); 127 | }); 128 | }; 129 | 130 | Commands.prototype._getMetadata = function(spaceName, indexName){ 131 | var _this = this; 132 | var spName = this.namespace[spaceName] // reduce overhead of lookup 133 | if (spName) 134 | { 135 | spaceName = spName.id; 136 | } 137 | if (typeof(spName) != 'undefined' && typeof(spName.indexes[indexName])!='undefined') 138 | { 139 | indexName = spName.indexes[indexName]; 140 | } 141 | if (typeof(spaceName)=='string' && typeof(indexName)=='string') 142 | { 143 | return this._getSpaceId(spaceName) 144 | .then(function(spaceId){ 145 | return Promise.all([spaceId, _this._getIndexId(spaceId, indexName)]); 146 | }); 147 | } 148 | var promises = []; 149 | if (typeof(spaceName) == 'string') 150 | promises.push(this._getSpaceId(spaceName)); 151 | else 152 | promises.push(spaceName); 153 | if (typeof(indexName) == 'string') 154 | promises.push(this._getIndexId(spaceName, indexName)); 155 | else 156 | promises.push(indexName); 157 | return Promise.all(promises); 158 | }; 159 | 160 | Commands.prototype.ping = function(){ 161 | var _this = this; 162 | return new Promise(function (resolve, reject) { 163 | var reqId = _this._getRequestId(); 164 | var len = 9; 165 | var buffer = createBuffer(len+5); 166 | 167 | buffer[0] = 0xce; 168 | buffer.writeUInt32BE(len, 1); 169 | buffer[5] = 0x82; 170 | buffer[6] = tarantoolConstants.KeysCode.code; 171 | buffer[7] = tarantoolConstants.RequestCode.rqPing; 172 | buffer[8] = tarantoolConstants.KeysCode.sync; 173 | buffer[9] = 0xce; 174 | buffer.writeUInt32BE(reqId, 10); 175 | 176 | _this.sendCommand([ 177 | tarantoolConstants.RequestCode.rqPing, 178 | reqId, 179 | {resolve: resolve, reject: reject} 180 | ], buffer); 181 | }); 182 | }; 183 | 184 | Commands.prototype.selectCb = function(spaceId, indexId, limit, offset, iterator, key, success, error){ 185 | if (!(key instanceof Array)) 186 | key = [key]; 187 | 188 | var _this = this; 189 | 190 | if (typeof(spaceId) == 'string' && _this.namespace[spaceId]) 191 | spaceId = _this.namespace[spaceId].id; 192 | if (typeof(indexId)=='string' && _this.namespace[spaceId] && _this.namespace[spaceId].indexes[indexId]) 193 | indexId = _this.namespace[spaceId].indexes[indexId]; 194 | if (typeof(spaceId)=='string' || typeof(indexId)=='string') 195 | { 196 | return _this._getMetadata(spaceId, indexId) 197 | .then(function(info){ 198 | return _this.selectCb(info[0], info[1], limit, offset, iterator, key, success, error); 199 | }) 200 | .catch(error); 201 | } 202 | 203 | var reqId = this._getRequestId(); 204 | if (iterator == 'all') 205 | key = []; 206 | var bufKey = msgpackEncode(key, {codec}); 207 | var len = 31+bufKey.length; 208 | var buffer = createBuffer(5+len); 209 | 210 | buffer[0] = 0xce; 211 | buffer.writeUInt32BE(len, 1); 212 | buffer[5] = 0x82; 213 | buffer[6] = tarantoolConstants.KeysCode.code; 214 | buffer[7] = tarantoolConstants.RequestCode.rqSelect; 215 | buffer[8] = tarantoolConstants.KeysCode.sync; 216 | buffer[9] = 0xce; 217 | buffer.writeUInt32BE(reqId, 10); 218 | buffer[14] = 0x86; 219 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 220 | buffer[16] = 0xcd; 221 | buffer.writeUInt16BE(spaceId, 17); 222 | buffer[19] = tarantoolConstants.KeysCode.index_id; 223 | buffer.writeUInt8(indexId, 20); 224 | buffer[21] = tarantoolConstants.KeysCode.limit; 225 | buffer[22] = 0xce; 226 | buffer.writeUInt32BE(limit, 23); 227 | buffer[27] = tarantoolConstants.KeysCode.offset; 228 | buffer[28] = 0xce; 229 | buffer.writeUInt32BE(offset, 29); 230 | buffer[33] = tarantoolConstants.KeysCode.iterator; 231 | buffer.writeUInt8(tarantoolConstants.IteratorsType[iterator], 34); 232 | buffer[35] = tarantoolConstants.KeysCode.key; 233 | bufKey.copy(buffer, 36); 234 | 235 | this.sendCommand([ 236 | tarantoolConstants.RequestCode.rqSelect, 237 | reqId, 238 | {resolve: success, reject: error} 239 | ], buffer); 240 | }; 241 | 242 | Commands.prototype.delete = function(spaceId, indexId, key){ 243 | var _this = this; 244 | if (Number.isInteger(key)) 245 | key = [key]; 246 | return new Promise(function (resolve, reject) { 247 | if (Array.isArray(key)) 248 | { 249 | if (typeof(spaceId)=='string' || typeof(indexId)=='string') 250 | { 251 | return _this._getMetadata(spaceId, indexId) 252 | .then(function(info){ 253 | return _this.delete(info[0], info[1], key); 254 | }) 255 | .then(resolve) 256 | .catch(reject); 257 | } 258 | var reqId = _this._getRequestId(); 259 | var bufKey = msgpackEncode(key, {codec}); 260 | 261 | var len = 17+bufKey.length; 262 | var buffer = createBuffer(5+len); 263 | 264 | buffer[0] = 0xce; 265 | buffer.writeUInt32BE(len, 1); 266 | buffer[5] = 0x82; 267 | buffer[6] = tarantoolConstants.KeysCode.code; 268 | buffer[7] = tarantoolConstants.RequestCode.rqDelete; 269 | buffer[8] = tarantoolConstants.KeysCode.sync; 270 | buffer[9] = 0xce; 271 | buffer.writeUInt32BE(reqId, 10); 272 | buffer[14] = 0x83; 273 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 274 | buffer[16] = 0xcd; 275 | buffer.writeUInt16BE(spaceId, 17); 276 | buffer[19] = tarantoolConstants.KeysCode.index_id; 277 | buffer.writeUInt8(indexId, 20); 278 | buffer[21] = tarantoolConstants.KeysCode.key; 279 | bufKey.copy(buffer, 22); 280 | 281 | _this.sendCommand([ 282 | tarantoolConstants.RequestCode.rqDelete, 283 | reqId, 284 | {resolve: resolve, reject: reject} 285 | ], buffer); 286 | } 287 | else 288 | reject(new TarantoolError('need array')); 289 | }); 290 | }; 291 | 292 | Commands.prototype.update = function(spaceId, indexId, key, ops){ 293 | var _this = this; 294 | if (Number.isInteger(key)) 295 | key = [key]; 296 | return new Promise(function (resolve, reject) { 297 | if (Array.isArray(ops) && Array.isArray(key)){ 298 | if (typeof(spaceId)=='string' || typeof(indexId)=='string') 299 | { 300 | return _this._getMetadata(spaceId, indexId) 301 | .then(function(info){ 302 | return _this.update(info[0], info[1], key, ops); 303 | }) 304 | .then(resolve) 305 | .catch(reject); 306 | } 307 | var reqId = _this._getRequestId(); 308 | var bufKey = msgpackEncode(key, {codec}); 309 | var bufOps = msgpackEncode(ops, {codec}); 310 | 311 | var len = 18+bufKey.length+bufOps.length; 312 | var buffer = createBuffer(len+5); 313 | 314 | buffer[0] = 0xce; 315 | buffer.writeUInt32BE(len, 1); 316 | buffer[5] = 0x82; 317 | buffer[6] = tarantoolConstants.KeysCode.code; 318 | buffer[7] = tarantoolConstants.RequestCode.rqUpdate; 319 | buffer[8] = tarantoolConstants.KeysCode.sync; 320 | buffer[9] = 0xce; 321 | buffer.writeUInt32BE(reqId, 10); 322 | buffer[14] = 0x84; 323 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 324 | buffer[16] = 0xcd; 325 | buffer.writeUInt16BE(spaceId, 17); 326 | buffer[19] = tarantoolConstants.KeysCode.index_id; 327 | buffer.writeUInt8(indexId, 20); 328 | buffer[21] = tarantoolConstants.KeysCode.key; 329 | bufKey.copy(buffer, 22); 330 | buffer[22+bufKey.length] = tarantoolConstants.KeysCode.tuple; 331 | bufOps.copy(buffer, 23+bufKey.length); 332 | 333 | _this.sendCommand([ 334 | tarantoolConstants.RequestCode.rqUpdate, 335 | reqId, 336 | {resolve: resolve, reject: reject} 337 | ], buffer); 338 | } 339 | else 340 | reject(new TarantoolError('need array')); 341 | }); 342 | }; 343 | 344 | Commands.prototype.upsert = function(spaceId, ops, tuple){ 345 | var _this = this; 346 | return new Promise(function (resolve, reject) { 347 | if (Array.isArray(ops)){ 348 | if (typeof(spaceId)=='string') 349 | { 350 | return _this._getMetadata(spaceId, 0) 351 | .then(function(info){ 352 | return _this.upsert(info[0], ops, tuple); 353 | }) 354 | .then(resolve) 355 | .catch(reject); 356 | } 357 | var reqId = _this._getRequestId(); 358 | var bufTuple = msgpackEncode(tuple, {codec}); 359 | var bufOps = msgpackEncode(ops, {codec}); 360 | 361 | var len = 16+bufTuple.length+bufOps.length; 362 | var buffer = createBuffer(len+5); 363 | 364 | buffer[0] = 0xce; 365 | buffer.writeUInt32BE(len, 1); 366 | buffer[5] = 0x82; 367 | buffer[6] = tarantoolConstants.KeysCode.code; 368 | buffer[7] = tarantoolConstants.RequestCode.rqUpsert; 369 | buffer[8] = tarantoolConstants.KeysCode.sync; 370 | buffer[9] = 0xce; 371 | buffer.writeUInt32BE(reqId, 10); 372 | buffer[14] = 0x83; 373 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 374 | buffer[16] = 0xcd; 375 | buffer.writeUInt16BE(spaceId, 17); 376 | buffer[19] = tarantoolConstants.KeysCode.tuple; 377 | bufTuple.copy(buffer, 20); 378 | buffer[20+bufTuple.length] = tarantoolConstants.KeysCode.def_tuple; 379 | bufOps.copy(buffer, 21+bufTuple.length); 380 | 381 | _this.sendCommand([ 382 | tarantoolConstants.RequestCode.rqUpsert, 383 | reqId, 384 | {resolve: resolve, reject: reject} 385 | ], buffer); 386 | } 387 | else 388 | reject(new TarantoolError('need ops array')); 389 | }); 390 | }; 391 | 392 | 393 | Commands.prototype.eval = function(expression){ 394 | var _this = this; 395 | var tuple = Array.prototype.slice.call(arguments, 1); 396 | return new Promise(function (resolve, reject) { 397 | var reqId = _this._getRequestId(); 398 | var bufExp = msgpackEncode(expression); 399 | var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); 400 | var len = 12+bufExp.length + bufTuple.length; 401 | var buffer = createBuffer(len+5); 402 | 403 | buffer[0] = 0xce; 404 | buffer.writeUInt32BE(len, 1); 405 | buffer[5] = 0x82; 406 | buffer[6] = tarantoolConstants.KeysCode.code; 407 | buffer[7] = tarantoolConstants.RequestCode.rqEval; 408 | buffer[8] = tarantoolConstants.KeysCode.sync; 409 | buffer[9] = 0xce; 410 | buffer.writeUInt32BE(reqId, 10); 411 | buffer[14] = 0x82; 412 | buffer.writeUInt8(tarantoolConstants.KeysCode.expression, 15); 413 | bufExp.copy(buffer, 16); 414 | buffer[16+bufExp.length] = tarantoolConstants.KeysCode.tuple; 415 | bufTuple.copy(buffer, 17+bufExp.length); 416 | 417 | _this.sendCommand([ 418 | tarantoolConstants.RequestCode.rqEval, 419 | reqId, 420 | {resolve: resolve, reject: reject} 421 | ], buffer); 422 | }); 423 | }; 424 | 425 | Commands.prototype.call = function(functionName){ 426 | var _this = this; 427 | var tuple = arguments.length > 1 ? Array.prototype.slice.call(arguments, 1) : []; 428 | return new Promise(function (resolve, reject) { 429 | var reqId = _this._getRequestId(); 430 | var bufName = msgpackEncode(functionName); 431 | var bufTuple = msgpackEncode(tuple ? tuple : [], {codec}); 432 | var len = 12+bufName.length + bufTuple.length; 433 | var buffer = createBuffer(len+5); 434 | 435 | buffer[0] = 0xce; 436 | buffer.writeUInt32BE(len, 1); 437 | buffer[5] = 0x82; 438 | buffer[6] = tarantoolConstants.KeysCode.code; 439 | buffer[7] = tarantoolConstants.RequestCode.rqCall; 440 | buffer[8] = tarantoolConstants.KeysCode.sync; 441 | buffer[9] = 0xce; 442 | buffer.writeUInt32BE(reqId, 10); 443 | buffer[14] = 0x82; 444 | buffer.writeUInt8(tarantoolConstants.KeysCode.function_name, 15); 445 | bufName.copy(buffer, 16); 446 | buffer[16+bufName.length] = tarantoolConstants.KeysCode.tuple; 447 | bufTuple.copy(buffer, 17+bufName.length); 448 | 449 | _this.sendCommand([ 450 | tarantoolConstants.RequestCode.rqCall, 451 | reqId, 452 | {resolve: resolve, reject: reject} 453 | ], buffer); 454 | }); 455 | }; 456 | 457 | Commands.prototype.sql = function(query, bindParams = []){ 458 | var _this = this; 459 | return new Promise(function (resolve, reject) { 460 | var reqId = _this._getRequestId(); 461 | var bufQuery = msgpackEncode(query); 462 | 463 | var bufParams = msgpackEncode(bindParams, {codec}); 464 | var len = 12+bufQuery.length + bufParams.length; 465 | var buffer = createBuffer(len+5); 466 | 467 | buffer[0] = 0xce; 468 | buffer.writeUInt32BE(len, 1); 469 | buffer[5] = 0x82; 470 | buffer[6] = tarantoolConstants.KeysCode.code; 471 | buffer[7] = tarantoolConstants.RequestCode.rqExecute; 472 | buffer[8] = tarantoolConstants.KeysCode.sync; 473 | buffer[9] = 0xce; 474 | buffer.writeUInt32BE(reqId, 10); 475 | buffer[14] = 0x82; 476 | buffer.writeUInt8(tarantoolConstants.KeysCode.sql_text, 15); 477 | bufQuery.copy(buffer, 16); 478 | buffer[16+bufQuery.length] = tarantoolConstants.KeysCode.sql_bind; 479 | bufParams.copy(buffer, 17+bufQuery.length); 480 | 481 | _this.sendCommand([ 482 | tarantoolConstants.RequestCode.rqExecute, 483 | reqId, 484 | {resolve: resolve, reject: reject} 485 | ], buffer); 486 | }); 487 | }; 488 | 489 | Commands.prototype.insert = function(spaceId, tuple){ 490 | var reqId = this._getRequestId(); 491 | return this._replaceInsert(tarantoolConstants.RequestCode.rqInsert, reqId, spaceId, tuple); 492 | }; 493 | 494 | Commands.prototype.replace = function(spaceId, tuple){ 495 | var reqId = this._getRequestId(); 496 | return this._replaceInsert(tarantoolConstants.RequestCode.rqReplace, reqId, spaceId, tuple); 497 | }; 498 | 499 | Commands.prototype._replaceInsert = function(cmd, reqId, spaceId, tuple){ 500 | var _this = this; 501 | return new Promise(function (resolve, reject) { 502 | if (Array.isArray(tuple)){ 503 | if (typeof(spaceId)=='string') 504 | { 505 | return _this._getMetadata(spaceId, 0) 506 | .then(function(info){ 507 | return _this._replaceInsert(cmd, reqId, info[0], tuple); 508 | }) 509 | .then(resolve) 510 | .catch(reject); 511 | } 512 | var bufTuple = msgpackEncode(tuple); 513 | var len = 15+bufTuple.length; 514 | var buffer = createBuffer(len+5); 515 | 516 | buffer[0] = 0xce; 517 | buffer.writeUInt32BE(len, 1); 518 | buffer[5] = 0x82; 519 | buffer[6] = tarantoolConstants.KeysCode.code; 520 | buffer[7] = cmd; 521 | buffer[8] = tarantoolConstants.KeysCode.sync; 522 | buffer[9] = 0xce; 523 | buffer.writeUInt32BE(reqId, 10); 524 | buffer[14] = 0x82; 525 | buffer.writeUInt8(tarantoolConstants.KeysCode.space_id, 15); 526 | buffer[16] = 0xcd; 527 | buffer.writeUInt16BE(spaceId, 17); 528 | buffer[19] = tarantoolConstants.KeysCode.tuple; 529 | bufTuple.copy(buffer, 20); 530 | 531 | _this.sendCommand([ 532 | cmd, 533 | reqId, 534 | {resolve: resolve, reject: reject} 535 | ], buffer); 536 | } 537 | else 538 | reject(new TarantoolError('need array')); 539 | }); 540 | }; 541 | 542 | Commands.prototype._auth = function(username, password){ 543 | var _this = this; 544 | return new Promise(function (resolve, reject) { 545 | var reqId = _this._getRequestId(); 546 | 547 | var user = msgpackEncode(username); 548 | var scrambled = scramble(password, _this.salt); 549 | var len = 44+user.length; 550 | var buffer = createBuffer(len+5); 551 | 552 | buffer[0] = 0xce; 553 | buffer.writeUInt32BE(len, 1); 554 | buffer[5] = 0x82; 555 | buffer[6] = tarantoolConstants.KeysCode.code; 556 | buffer[7] = tarantoolConstants.RequestCode.rqAuth; 557 | buffer[8] = tarantoolConstants.KeysCode.sync; 558 | buffer[9] = 0xce; 559 | buffer.writeUInt32BE(reqId, 10); 560 | buffer[14] = 0x82; 561 | buffer.writeUInt8(tarantoolConstants.KeysCode.username, 15); 562 | user.copy(buffer, 16); 563 | buffer[16+user.length] = tarantoolConstants.KeysCode.tuple; 564 | buffer[17+user.length] = 0x92; 565 | tarantoolConstants.passEnter.copy(buffer, 18+user.length); 566 | buffer[28+user.length] = 0xb4; 567 | scrambled.copy(buffer, 29+user.length); 568 | 569 | _this.commandsQueue.push([ 570 | tarantoolConstants.RequestCode.rqAuth, 571 | reqId, 572 | {resolve: resolve, reject: reject}, 573 | ]); 574 | _this.socket.write(buffer); 575 | }); 576 | }; 577 | 578 | function shatransform(t){ 579 | return createHash('sha1').update(t).digest(); 580 | } 581 | 582 | function xor(a, b) { 583 | if (!Buffer.isBuffer(a)) a = bufferFrom(a); 584 | if (!Buffer.isBuffer(b)) b = bufferFrom(b); 585 | var res = []; 586 | var i; 587 | if (a.length > b.length) { 588 | for (i = 0; i < b.length; i++) { 589 | res.push(a[i] ^ b[i]); 590 | } 591 | } else { 592 | for (i = 0; i < a.length; i++) { 593 | res.push(a[i] ^ b[i]); 594 | } 595 | } 596 | return bufferFrom(res); 597 | } 598 | 599 | function scramble(password, salt){ 600 | var encSalt = bufferFrom(salt, 'base64'); 601 | var step1 = shatransform(password); 602 | var step2 = shatransform(step1); 603 | var step3 = shatransform(Buffer.concat([encSalt[bufferSubarrayPoly](0, 20), step2])); 604 | return xor(step1, step3); 605 | } 606 | 607 | module.exports = Commands; -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by klond on 05.04.15. 3 | */ 4 | 5 | /*eslint-env mocha */ 6 | /* global Promise */ 7 | var exec = require('child_process').exec; 8 | var expect = require('chai').expect; 9 | var sinon = require('sinon'); 10 | var spy = sinon.spy.bind(sinon); 11 | var stub = sinon.stub.bind(sinon); 12 | 13 | var fs = require('fs'); 14 | var assert = require('assert'); 15 | var TarantoolConnection = require('../lib/connection'); 16 | var SliderBuffer = require('../lib/sliderBuffer'); 17 | var mlite = require('msgpack-lite'); 18 | var conn; 19 | 20 | describe('constructor', function () { 21 | it('should parse options correctly', function () { 22 | stub(TarantoolConnection.prototype, 'connect').returns(Promise.resolve()); 23 | var option; 24 | try { 25 | option = getOption(6380); 26 | expect(option).to.have.property('port', 6380); 27 | expect(option).to.have.property('host', 'localhost'); 28 | 29 | option = getOption('6380'); 30 | expect(option).to.have.property('port', 6380); 31 | 32 | option = getOption(6381, '192.168.1.1'); 33 | expect(option).to.have.property('port', 6381); 34 | expect(option).to.have.property('host', '192.168.1.1'); 35 | 36 | option = getOption(6381, '192.168.1.1', { 37 | password: '123', 38 | username: 'userloser' 39 | }); 40 | expect(option).to.have.property('port', 6381); 41 | expect(option).to.have.property('host', '192.168.1.1'); 42 | expect(option).to.have.property('password', '123'); 43 | expect(option).to.have.property('username', 'userloser'); 44 | 45 | option = getOption('mail.ru:33013'); 46 | expect(option).to.have.property('port', 33013); 47 | expect(option).to.have.property('host', 'mail.ru'); 48 | 49 | option = getOption('notguest:sesame@mail.ru:3301'); 50 | expect(option).to.have.property('port', 3301); 51 | expect(option).to.have.property('host', 'mail.ru'); 52 | expect(option).to.have.property('username', 'notguest'); 53 | expect(option).to.have.property('password', 'sesame'); 54 | 55 | option = getOption('/var/run/tarantool/unix.sock'); 56 | expect(option).to.have.property('path', '/var/run/tarantool/unix.sock'); 57 | 58 | option = getOption({ 59 | port: 6380, 60 | host: '192.168.1.1' 61 | }); 62 | expect(option).to.have.property('port', 6380); 63 | expect(option).to.have.property('host', '192.168.1.1'); 64 | 65 | option = getOption({ 66 | port: 6380, 67 | host: '192.168.1.1', 68 | reserveHosts: ['notguest:sesame@mail.ru:3301', 'mail.ru:3301'] 69 | }); 70 | expect(option).to.have.property('port', 6380); 71 | expect(option).to.have.property('host', '192.168.1.1'); 72 | expect(option).to.have.property('reserveHosts'); 73 | expect(option.reserveHosts).to.deep.equal(['notguest:sesame@mail.ru:3301', 'mail.ru:3301']); 74 | 75 | option = new TarantoolConnection({ 76 | port: 6380, 77 | host: '192.168.1.1', 78 | reserveHosts: ['notguest:sesame@mail.ru:3301', 'mail.ru:3301'] 79 | }); 80 | expect(option.reserve).to.deep.include( 81 | { 82 | port: 6380, 83 | host: '192.168.1.1', 84 | path: null, 85 | username: null, 86 | password: null 87 | }, 88 | { 89 | port: 3301, 90 | host: 'mail.ru' 91 | }, 92 | { 93 | port: 3301, 94 | host: 'mail.ru', 95 | username: 'notguest', 96 | password: 'sesame' 97 | } 98 | ); 99 | 100 | option = getOption({ 101 | port: 6380, 102 | host: '192.168.1.1' 103 | }); 104 | expect(option).to.have.property('port', 6380); 105 | expect(option).to.have.property('host', '192.168.1.1'); 106 | 107 | option = getOption({ 108 | port: '6380' 109 | }); 110 | expect(option).to.have.property('port', 6380); 111 | 112 | option = getOption(6380, { 113 | host: '192.168.1.1' 114 | }); 115 | expect(option).to.have.property('port', 6380); 116 | expect(option).to.have.property('host', '192.168.1.1'); 117 | 118 | option = getOption('6380', { 119 | host: '192.168.1.1' 120 | }); 121 | expect(option).to.have.property('port', 6380); 122 | } catch (err) { 123 | TarantoolConnection.prototype.connect.restore(); 124 | throw err; 125 | } 126 | TarantoolConnection.prototype.connect.restore(); 127 | 128 | function getOption() { 129 | conn = TarantoolConnection.apply(null, arguments); 130 | return conn.options; 131 | } 132 | }); 133 | 134 | it('should throw when arguments is invalid', function () { 135 | expect(function () { 136 | new TarantoolConnection(function () {}); 137 | }).to.throw(Error); 138 | }); 139 | }); 140 | 141 | describe('reconnecting', function () { 142 | this.timeout(8000); 143 | it('should pass the correct retry times', function (done) { 144 | var t = 0; 145 | new TarantoolConnection({ 146 | port: 1, 147 | retryStrategy: function (times) { 148 | expect(times).to.eql(++t); 149 | if (times === 3) { 150 | done(); 151 | return; 152 | } 153 | return 0; 154 | } 155 | }); 156 | }); 157 | 158 | it('should skip reconnecting when retryStrategy doesn\'t return a number', function (done) { 159 | conn = new TarantoolConnection({ 160 | port: 1, 161 | retryStrategy: function () { 162 | process.nextTick(function () { 163 | expect(conn.state).to.eql(32); // states.END == 32 164 | done(); 165 | }); 166 | return null; 167 | } 168 | }); 169 | }); 170 | 171 | it('should not try to reconnect when disconnected manually', function (done) { 172 | conn = new TarantoolConnection(33013, { lazyConnect: true }); 173 | conn.eval('return func_foo()') 174 | .then(function () { 175 | conn.disconnect(); 176 | return conn.eval('return func_foo()'); 177 | }) 178 | .catch(function (err) { 179 | expect(err.message).to.match(/Connection is closed/); 180 | done(); 181 | }); 182 | }); 183 | it('should try to reconnect and then connect eventially', function (done){ 184 | function timer(){ 185 | return conn.ping() 186 | .then(function(res){ 187 | assert.equal(res, true); 188 | done(); 189 | }) 190 | .catch(function(err){ 191 | done(err); 192 | }); 193 | } 194 | conn = new TarantoolConnection(33013, { lazyConnect: true }); 195 | conn.eval('return func_foo()') 196 | .then(function () { 197 | exec('docker kill tarantool', function(error, stdout, stderr){ 198 | if(error){ 199 | done(error); 200 | } 201 | conn.eval('return func_foo()') 202 | .catch(function(err){ 203 | expect(err.message).to.match(/connect ECONNREFUSED/); 204 | }); 205 | exec('docker start tarantool', function(e, stdo, stde){ 206 | if(error){ 207 | done(error); 208 | } 209 | setTimeout(timer, 1000); 210 | }); 211 | }); 212 | }); 213 | }); 214 | }); 215 | 216 | describe('multihost', function () { 217 | this.timeout(10000); 218 | // after(function() { 219 | // exec('docker start tarantool'); 220 | // }); 221 | var t; 222 | it('should try to connect to reserve hosts cyclically', function(done){ 223 | conn = new TarantoolConnection(33013, { 224 | reserveHosts: ['test:test@127.0.0.1:33014', '127.0.0.1:33015'], 225 | beforeReserve: 1, 226 | retryStrategy: function (times) { 227 | return Math.min(times * 500, 2000); 228 | } 229 | }); 230 | t = 0; 231 | conn.on('connect', function(){ 232 | switch (t){ 233 | case 1: 234 | conn.eval('return box.cfg') 235 | .then(function(res){ 236 | t++; 237 | expect(res[0].listen.toString()).to.eql('33014'); 238 | exec('docker kill reserve', function(error, stdout, stderr){ 239 | if(error){ 240 | done(error); 241 | } 242 | }); 243 | exec('docker start tarantool'); 244 | }) 245 | .catch(function(e){ 246 | done(e); 247 | }); 248 | break; 249 | case 2: 250 | conn.eval('return box.cfg') 251 | .then(function(res){ 252 | t++; 253 | expect(res[0].listen.toString()).to.eql('33015'); 254 | exec('docker kill reserve_2', function(error, stdout, stderr){ 255 | if(error){ 256 | done(error); 257 | } 258 | }); 259 | }) 260 | .catch(function(e){ 261 | done(e); 262 | }); 263 | break; 264 | case 3: 265 | conn.eval('return box.cfg') 266 | .then(function(res){ 267 | t++; 268 | expect(res[0].listen.toString()).to.eql('33013'); 269 | done(); 270 | }) 271 | .catch(function(e){ 272 | done(e); 273 | }); 274 | break; 275 | } 276 | }); 277 | conn.ping() 278 | .then(function(){ 279 | t++; 280 | exec('docker kill tarantool', function(error, stdout, stderr){ 281 | if(error){ 282 | done(error); 283 | } 284 | }); 285 | }) 286 | .catch(function(e){ 287 | done(e); 288 | }); 289 | }); 290 | }); 291 | 292 | describe('lazy connect', function(){ 293 | beforeEach(function(){ 294 | conn = new TarantoolConnection({port: 33013, lazyConnect: true, username: 'test', password: 'test'}); 295 | }); 296 | it('lazy connect', function(done){ 297 | conn.connect() 298 | .then(function(){ 299 | done(); 300 | }, function(e){ 301 | done(e); 302 | }); 303 | }); 304 | it('should be authenticated', function(done){ 305 | conn.connect().then(function(){ 306 | return conn.eval('return box.session.user()'); 307 | }) 308 | .then(function(res){ 309 | assert.equal(res[0], 'test'); 310 | done(); 311 | }) 312 | .catch(function(e){done(e);}); 313 | }); 314 | it('should disconnect when inited', function(done){ 315 | conn.disconnect(); 316 | expect(conn.state).to.eql(32); // states.END == 32 317 | done(); 318 | }); 319 | it('should disconnect', function(done){ 320 | conn.connect() 321 | .then(function(res){ 322 | conn.disconnect(); 323 | assert.equal(conn.socket.writable, false); 324 | done(); 325 | }) 326 | .catch(function(e){done(e);}); 327 | }); 328 | }); 329 | describe('instant connection', function(){ 330 | beforeEach(function(){ 331 | conn = new TarantoolConnection({port: 33013, username: 'test', password: 'test'}); 332 | }); 333 | it('connect', function(done){ 334 | conn.eval('return func_arg(...)', 'connected!') 335 | .then(function(res){ 336 | try{ 337 | assert.equal(res, 'connected!'); 338 | } catch(e){console.error(e);} 339 | done(); 340 | }, function(e){ 341 | done(e); 342 | }); 343 | }); 344 | it('should reject when connected', function (done) { 345 | conn.connect().catch(function (err) { 346 | expect(err.message).to.match(/Tarantool is already connecting\/connected/); 347 | done(); 348 | }); 349 | }); 350 | it('should be authenticated', function(done){ 351 | conn.eval('return box.session.user()') 352 | .then(function(res){ 353 | assert.equal(res[0], 'test'); 354 | done(); 355 | }) 356 | .catch(function(e){done(e);}); 357 | }); 358 | it('should reject when auth failed', function (done) { 359 | conn = new TarantoolConnection({port: 33013, username: 'userloser', password: 'test'}); 360 | conn.eval('return func_foo()') 361 | .catch(function (err) { 362 | expect(err.message).to.include("not found"); 363 | conn.disconnect(); 364 | done(); 365 | }); 366 | }); 367 | it('should reject command when connection is closed', function (done) { 368 | conn = new TarantoolConnection(); 369 | conn.disconnect(); 370 | conn.eval('return func_foo()') 371 | .catch(function (err) { 372 | expect(err.message).to.match(/Connection is closed/); 373 | done(); 374 | }); 375 | }); 376 | }); 377 | 378 | describe('timeout', function(){ 379 | it('should close the connection when timeout', function (done) { 380 | conn = new TarantoolConnection(33013, '192.0.0.0', { 381 | timeout: 1, 382 | retryStrategy: null 383 | }); 384 | var pending = 2; 385 | conn.on('error', function (err) { 386 | expect(err.message).to.eql('connect ETIMEDOUT'); 387 | if (!--pending) { 388 | done(); 389 | } 390 | }); 391 | conn.ping() 392 | .catch(function (err) { 393 | expect(err.message).to.match(/Connection is closed/); 394 | if (!--pending) { 395 | done(); 396 | } 397 | }); 398 | }); 399 | it('should clear the timeout when connected', function (done) { 400 | conn = new TarantoolConnection(33013, { timeout: 10000 }); 401 | setImmediate(function () { 402 | stub(conn.socket, 'setTimeout') 403 | .callsFake(function (timeout) { 404 | expect(timeout).to.eql(0); 405 | conn.socket.setTimeout.restore(); 406 | done(); 407 | }); 408 | }); 409 | }); 410 | }); 411 | 412 | 413 | describe('requests', function(){ 414 | var insertTuple = [50, 10, 'my key', 30]; 415 | before(function(done){ 416 | console.log('before call'); 417 | try{ 418 | conn = new TarantoolConnection({port: 33013, username: 'test', password: 'test'}); 419 | 420 | Promise.all([conn.delete(514, 0, [1]),conn.delete(514, 0, [2]), 421 | conn.delete(514, 0, [3]),conn.delete(514, 0, [4]), 422 | conn.delete(512, 0, [999])]) 423 | .then(function(){ 424 | return conn.call('clearaddmore'); 425 | }) 426 | .then(function(){ 427 | done(); 428 | }) 429 | .catch(function(e){ 430 | done(e); 431 | }); 432 | } 433 | catch(e){ 434 | console.log(e); 435 | } 436 | }); 437 | it('replace', function(done){ 438 | conn.replace(512, insertTuple) 439 | .then(function(a){ 440 | assert.equal(a.length, 1); 441 | for (var i = 0; i