Skip to content

Commit ed972dd

Browse files
authored
Merge pull request #53 from goodwise/master
New msgpack extensions, extended connection options, bug fixes
2 parents 388ccf5 + ca3fde4 commit ed972dd

17 files changed

+827
-254
lines changed

README.md

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Code architecture and some features in version 3 borrowed from the [ioredis](htt
2626

2727
## Installation
2828

29-
```
29+
```Bash
3030
npm install --save tarantool-driver
3131
```
3232
## Configuration
@@ -38,18 +38,26 @@ Creates a Tarantool instance, extends [EventEmitter](http://nodejs.org/api/event
3838
Connection related custom events:
3939
* "reconnecting" - emitted when the client try to reconnect, first argument is retry delay in ms.
4040
* "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.
4142

4243
| Param | Type | Default | Description |
4344
| --- | --- | --- | --- |
4445
| [port] | <code>number</code> \| <code>string</code> \| <code>Object</code> | <code>3301</code> | 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). |
4546
| [host] | <code>string</code> \| <code>Object</code> | <code>&quot;localhost&quot;</code> | Host of the Tarantool server, when the first argument is a URL string, this argument is an object represents the options. |
46-
| [options] | <code>Object</code> | | Other options. |
47+
| [path] | <code>string</code> \| <code>Object</code> | <code>null</code> | Unix socket path of the Tarantool server. |
48+
| [options] | <code>Object</code> | | Other options, including all from [net.createConnection](https://nodejs.org/api/net.html#netcreateconnection). |
4749
| [options.port] | <code>number</code> | <code>6379</code> | Port of the Tarantool server. |
4850
| [options.host] | <code>string</code> | <code>&quot;localhost&quot;</code> | Host of the Tarantool server. |
4951
| [options.username] | <code>string</code> | <code>null</code> | If set, client will authenticate with the value of this option when connected. |
5052
| [options.password] | <code>string</code> | <code>null</code> | If set, client will authenticate with the value of this option when connected. |
5153
| [options.timeout] | <code>number</code> | <code>0</code> | The milliseconds before a timeout occurs during the initial connection to the Tarantool server. |
54+
| [options.tls] | <code>Object</code> | <code>null</code> | 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] | <code>boolean</code> | <code>true</code> | Enables keep-alive functionality (recommended). |
56+
| [options.noDelay] | <code>boolean</code> | <code>true</code> | Disables the use of Nagle's algorithm (recommended). |
5257
| [options.lazyConnect] | <code>boolean</code> | <code>false</code> | 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] | <code>string</code> | <code>null</code> | What to do when Tarantool server rejects write operation, e.g. because of `box.cfg.read_only` set to `true` or during snapshot fetching. <br /> Possible values are: <br /> - `null`: just rejects Promise with an error <br /> - `changeHost`: disconnect from the current host and connect to the next from `reserveHosts`. Pending Promise will be rejected. <br /> - `changeAndRetry`: same as `changeHost`, but after reconnecting tries to run the command again in order to fullfil the Promise |
59+
| [options.maxRetriesPerRequest] | <code>number</code> | <code>5</code> | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. |
60+
| [options.enableOfflineQueue] | <code>boolean</code> | <code>true</code> | 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. |
5361
| [options.reserveHosts] | <code>array</code> | [] | 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.|
5462
| [options.beforeReserve] | <code>number</code> | <code>2</code> | Number of attempts to reconnect before connect to next host from the <code>reserveHosts</code> |
5563
| [options.retryStrategy] | <code>function</code> | | See below |
@@ -107,7 +115,7 @@ will be lost forever if the user doesn't call `tarantool.connect()` manually.
107115
## Usage example
108116

109117
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.
110-
```
118+
```javascript
111119
var TarantoolConnection = require('tarantool-driver');
112120
var conn = new TarantoolConnection('notguest:sesame@mail.ru:3301');
113121

@@ -123,8 +131,7 @@ conn.select(512, 0, 1, 0, 'eq', [50])
123131

124132
You can use any implementation that can be duck typing with next interface:
125133

126-
```
127-
134+
```Javascript
128135
//msgpack implementation example
129136
/*
130137
@interface
@@ -155,6 +162,24 @@ Resolve if connected. Or reject if not.
155162

156163
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)
157164

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+
158183
### tarantool.select(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple) ⇒ <code>Promise</code>
159184

160185
[Iterators](http://tarantool.org/doc/book/box/box_index.html). Available iterators: 'eq', 'req', 'all', 'lt', 'le', 'ge', 'gt', 'bitsAllSet', 'bitsAnySet', 'bitsAllNotSet'.
@@ -163,13 +188,28 @@ It's just select. Promise resolve array of tuples.
163188

164189
Some examples:
165190

166-
```
191+
```Javascript
167192
conn.select(512, 0, 1, 0, 'eq', [50]);
168193
//same as
169194
conn.select('test', 'primary', 1, 0, 'eq', [50]);
170195
```
171196

172-
You can use space name or index name instead of id, but it will some requests for get this metadata. That information actual for delete, replace, insert, update too.
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+
```
173213

174214
### tarantool.selectCb(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple, callback: function(success), callback: function(error))
175215

@@ -210,23 +250,23 @@ Promise resolve a new or replaced tuple.
210250
Call a function with arguments.
211251

212252
You can create function on tarantool side:
213-
```
253+
```Lua
214254
function myget(id)
215255
val = box.space.batched:select{id}
216256
return val[1]
217257
end
218258
```
219259

220260
And then use something like this:
221-
```
261+
```Javascript
222262
conn.call('myget', 4)
223263
.then(function(value){
224264
console.log(value);
225265
});
226266
```
227267

228268
If you have a 2 arguments function just send a second arguments in this way:
229-
```
269+
```Javascript
230270
conn.call('my2argumentsfunc', 'first', 'second argument')
231271
```
232272
And etc like this.
@@ -242,14 +282,14 @@ Promise resolve result:any.
242282
Example:
243283

244284

245-
```
285+
```Javascript
246286
conn.eval('return box.session.user()')
247287
.then(function(res){
248288
console.log('current user is:' res[0])
249289
})
250290
```
251291

252-
### tarantool.sql(query: String, bindParams: Array) -> <code>Promise</code>
292+
### tarantool.sql(query: String, bindParams: Array) <code>Promise</code>
253293

254294
It's accessible only in 2.1 tarantool.
255295

@@ -261,7 +301,7 @@ More about it [here](https://www.tarantool.io/en/doc/2.1/tutorials/sql_tutorial/
261301

262302
Example:
263303

264-
```
304+
```Javascript
265305
await connection.insert('tags', ['tag_1', 1])
266306
await connection.insert('tags', ['tag_2', 50])
267307
connection.sql('select * from "tags"')
@@ -277,6 +317,24 @@ P.S. If you using lowercase in your space name you need to use a double quote fo
277317

278318
It doesn't work for space without format.
279319

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+
280338
### tarantool.ping() ⇒ <code>Promise</code>
281339

282340
Promise resolve true.
@@ -299,10 +357,24 @@ It's ok you can do whatever you need. I add log options for some technical infor
299357

300358
## Changelog
301359

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+
302375
### 3.0.7
303376

304-
Fix in header decoding to support latest Tarantool versions.
305-
Update to tests to support latest Tarantool versions.
377+
Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions.
306378

307379
### 3.0.6
308380

@@ -348,4 +420,7 @@ Key is now can be just a number.
348420

349421
## ToDo
350422

351-
finish multihost feature
423+
1. Streams
424+
2. Events and subscriptions
425+
3. Graceful shutdown protocol
426+
4. Prepared SQL statements

benchmark/box.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
box.cfg{listen=3301}
22

3+
if not box.schema.user.exists('test') then
4+
box.schema.user.create('test')
5+
end
6+
37
user = box.user
48
if not user then
59
box.schema.user.grant('test', 'execute', 'universe')

benchmark/read.js

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,23 @@ var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}
1111

1212
conn.connect()
1313
.then(function(){
14-
1514
suite.add('select cb', {defer: true, fn: function(defer){
1615
function callback(){
1716
defer.resolve();
1817
}
19-
conn.selectCb(512, 0, 1, 0, 'eq', ['test'], callback, console.error);
18+
conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error);
2019
}});
2120

2221
suite.add('select promise', {defer: true, fn: function(defer){
23-
conn.select(512, 0, 1, 0, 'eq', ['test'])
22+
conn.select('counter', 0, 1, 0, 'eq', ['test'])
2423
.then(function(){ defer.resolve();});
2524
}});
2625

2726
suite.add('paralell 500', {defer: true, fn: function(defer){
2827
try{
2928
promises = [];
3029
for (let l=0;l<500;l++){
31-
promises.push(conn.select(512, 0, 1, 0, 'eq', ['test']));
30+
promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test']));
3231
}
3332
var chain = Promise.all(promises);
3433
chain.then(function(){ defer.resolve(); })
@@ -51,7 +50,7 @@ conn.connect()
5150
promises = [];
5251
for (var l=0;l<10;l++){
5352
promises.push(
54-
conn.select(512, 0, 1, 0, 'eq', ['test'])
53+
conn.select('counter', 0, 1, 0, 'eq', ['test'])
5554
);
5655
}
5756
return Promise.all(promises);
@@ -76,7 +75,7 @@ conn.connect()
7675
promises = [];
7776
for (var l=0;l<50;l++){
7877
promises.push(
79-
conn.select(512, 0, 1, 0, 'eq', ['test'])
78+
conn.select('counter', 0, 1, 0, 'eq', ['test'])
8079
);
8180
}
8281
return Promise.all(promises);
@@ -91,6 +90,31 @@ conn.connect()
9190
console.error(e, e.stack);
9291
}
9392
}});
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+
94118
suite
95119
.on('cycle', function(event) {
96120
console.log(String(event.target));

benchmark/write.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ conn.connect()
3535
console.error(e, e.stack);
3636
}
3737
}});
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+
3863
suite
3964
.on('cycle', function(event) {
4065
console.log(String(event.target));

0 commit comments

Comments
 (0)