Skip to content

New msgpack extensions, extended connection options, bug fixes #53

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 91 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Code architecture and some features in version 3 borrowed from the [ioredis](htt

## Installation

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

| Param | Type | Default | Description |
| --- | --- | --- | --- |
| [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). |
| [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. |
| [options] | <code>Object</code> | | Other options. |
| [path] | <code>string</code> \| <code>Object</code> | <code>null</code> | Unix socket path of the Tarantool server. |
| [options] | <code>Object</code> | | Other options, including all from [net.createConnection](https://nodejs.org/api/net.html#netcreateconnection). |
| [options.port] | <code>number</code> | <code>6379</code> | Port of the Tarantool server. |
| [options.host] | <code>string</code> | <code>&quot;localhost&quot;</code> | Host of the Tarantool server. |
| [options.username] | <code>string</code> | <code>null</code> | If set, client will authenticate with the value of this option when connected. |
| [options.password] | <code>string</code> | <code>null</code> | If set, client will authenticate with the value of this option when connected. |
| [options.timeout] | <code>number</code> | <code>0</code> | The milliseconds before a timeout occurs during the initial connection to the Tarantool server. |
| [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) |
| [options.keepAlive] | <code>boolean</code> | <code>true</code> | Enables keep-alive functionality (recommended). |
| [options.noDelay] | <code>boolean</code> | <code>true</code> | Disables the use of Nagle's algorithm (recommended). |
| [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. |
| [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 |
| [options.maxRetriesPerRequest] | <code>number</code> | <code>5</code> | Number of attempts to find the alive host if `nonWritableHostPolicy` is not null. |
| [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. |
| [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.|
| [options.beforeReserve] | <code>number</code> | <code>2</code> | Number of attempts to reconnect before connect to next host from the <code>reserveHosts</code> |
| [options.retryStrategy] | <code>function</code> | | See below |
Expand Down Expand Up @@ -107,7 +115,7 @@ will be lost forever if the user doesn't call `tarantool.connect()` manually.
## Usage example

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

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

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

```

```Javascript
//msgpack implementation example
/*
@interface
Expand Down Expand Up @@ -155,6 +162,24 @@ Resolve if connected. Or reject if not.

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)

### tarantool.packUuid(uuid: String)

**Method for converting [UUID values](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#uuid) to Tarantool-compatible format.**

If passing UUID without converion via this method, server will accept it as simple String.

### tarantool.packDecimal(numberToConvert: Number)

**Method for converting Numbers (Float or Integer) to Tarantool [Decimal](https://www.tarantool.io/ru/doc/latest/concepts/data_model/value_store/#decimal) type.**

If passing number without converion via this method, server will accept it as Integer or Double (for JS Float type).

### tarantool.packInteger(numberToConvert: Number)

**Method for safely passing numbers up to int64 to bind params**

Otherwise msgpack will encode anything bigger than int32 as a double number.

### tarantool.select(spaceId: Number or String, indexId: Number or String, limit: Number, offset: Number, iterator: Iterator, key: tuple) ⇒ <code>Promise</code>

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

Some examples:

```
```Javascript
conn.select(512, 0, 1, 0, 'eq', [50]);
//same as
conn.select('test', 'primary', 1, 0, 'eq', [50]);
```

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.
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.

For tests, we will create a Space named 'users' on the Tarantool server-side, where the 'id' index is of UUID type:

```lua
-- example schema of such space
box.schema.space.create("users", {engine = 'memtx'})
box.space.users:format({
{name = 'id', type = 'uuid', is_nullable = false},
{name = 'username', type = 'string', is_nullable = false}
})
```
And then select some tuples on a client side:
```Javascript
conn.select('users', 'id', 1, 0, 'eq', [conn.packUuid('550e8400-e29b-41d4-a716-446655440000')]);
```

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

Expand Down Expand Up @@ -210,23 +250,23 @@ Promise resolve a new or replaced tuple.
Call a function with arguments.

You can create function on tarantool side:
```
```Lua
function myget(id)
val = box.space.batched:select{id}
return val[1]
end
```

And then use something like this:
```
```Javascript
conn.call('myget', 4)
.then(function(value){
console.log(value);
});
```

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


```
```Javascript
conn.eval('return box.session.user()')
.then(function(res){
console.log('current user is:' res[0])
})
```

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

It's accessible only in 2.1 tarantool.

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

Example:

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

It doesn't work for space without format.

### tarantool.pipeline().<...>.exec()

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).
This way the performance is significantly improved by more than 300% (depending on the number of pipelined commands - the bigger, the better)

Example:

```Javascript
tarantool.pipeline()
.insert('tags', ['tag_1', 1])
.insert('tags', ['tag_2', 50])
.sql('update "tags" set "amount" = 10 where "tag_id" = \'tag_1\'')
.update('tags', 'tag_id', ['tag_2'], [['=', 'amount', 30]])
.sql('select * from "tags"')
.call('truncateTags')
.exec()
```

### tarantool.ping() ⇒ <code>Promise</code>

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

## Changelog

### 3.1.0

- Added 3 new msgpack extensions: UUID, Datetime, Decimal.
- Connection object now accepts all options of `net.createConnection()`, including Unix socket path.
- New `nonWritableHostPolicy` and related options, which improves a high availability capabilities without any 3rd parties.
- Ability to disable the offline queue.
- 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.
- `selectCb()` now also accepts `spaceId` and `indexId` as their String names, not only their IDs.
- Some performance improvements by caching internal values.
- TLS (SSL) support.
- 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).
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`.
Note that this feature doesn't replaces the Transaction model, which has some level of isolation.
- Changed `const` declaration to `var` in order to support old Node.JS versions.

### 3.0.7

Fix in header decoding to support latest Tarantool versions.
Update to tests to support latest Tarantool versions.
Fix in header decoding to support latest Tarantool versions. Update to tests to support latest Tarantool versions.

### 3.0.6

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

## ToDo

finish multihost feature
1. Streams
2. Events and subscriptions
3. Graceful shutdown protocol
4. Prepared SQL statements
4 changes: 4 additions & 0 deletions benchmark/box.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
box.cfg{listen=3301}

if not box.schema.user.exists('test') then
box.schema.user.create('test')
end

user = box.user
if not user then
box.schema.user.grant('test', 'execute', 'universe')
Expand Down
36 changes: 30 additions & 6 deletions benchmark/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,23 @@ var conn = new Driver(process.argv[process.argv.length - 1], {lazyConnect: true}

conn.connect()
.then(function(){

suite.add('select cb', {defer: true, fn: function(defer){
function callback(){
defer.resolve();
}
conn.selectCb(512, 0, 1, 0, 'eq', ['test'], callback, console.error);
conn.selectCb('counter', 0, 1, 0, 'eq', ['test'], callback, console.error);
}});

suite.add('select promise', {defer: true, fn: function(defer){
conn.select(512, 0, 1, 0, 'eq', ['test'])
conn.select('counter', 0, 1, 0, 'eq', ['test'])
.then(function(){ defer.resolve();});
}});

suite.add('paralell 500', {defer: true, fn: function(defer){
try{
promises = [];
for (let l=0;l<500;l++){
promises.push(conn.select(512, 0, 1, 0, 'eq', ['test']));
promises.push(conn.select('counter', 0, 1, 0, 'eq', ['test']));
}
var chain = Promise.all(promises);
chain.then(function(){ defer.resolve(); })
Expand All @@ -51,7 +50,7 @@ conn.connect()
promises = [];
for (var l=0;l<10;l++){
promises.push(
conn.select(512, 0, 1, 0, 'eq', ['test'])
conn.select('counter', 0, 1, 0, 'eq', ['test'])
);
}
return Promise.all(promises);
Expand All @@ -76,7 +75,7 @@ conn.connect()
promises = [];
for (var l=0;l<50;l++){
promises.push(
conn.select(512, 0, 1, 0, 'eq', ['test'])
conn.select('counter', 0, 1, 0, 'eq', ['test'])
);
}
return Promise.all(promises);
Expand All @@ -91,6 +90,31 @@ conn.connect()
console.error(e, e.stack);
}
}});

suite.add('pipelined select by 10', {defer: true, fn: function(defer){
var pipelinedConn = conn.pipeline()

for (var i=0;i<10;i++) {
pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']);
}

pipelinedConn.exec()
.then(function(){ defer.resolve(); })
.catch(function(e){ defer.reject(e); });
}});

suite.add('pipelined select by 50', {defer: true, fn: function(defer){
var pipelinedConn = conn.pipeline()

for (var i=0;i<50;i++) {
pipelinedConn.select('counter', 0, 1, 0, 'eq', ['test']);
}

pipelinedConn.exec()
.then(function(){ defer.resolve(); })
.catch(function(e){ defer.reject(e); });
}});

suite
.on('cycle', function(event) {
console.log(String(event.target));
Expand Down
25 changes: 25 additions & 0 deletions benchmark/write.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ conn.connect()
console.error(e, e.stack);
}
}});

suite.add('pipelined insert by 10', {defer: true, fn: function(defer){
var pipelinedConn = conn.pipeline()

for (var i=0;i<10;i++) {
pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}])
}

pipelinedConn.exec()
.then(function(){ defer.resolve(); })
.catch(function(e){ defer.reject(e); })
}});

suite.add('pipelined insert by 50', {defer: true, fn: function(defer){
var pipelinedConn = conn.pipeline()

for (var i=0;i<50;i++) {
pipelinedConn.insert('bench', [c++, {user: 'username', data: 'Some data.'}])
}

pipelinedConn.exec()
.then(function(){ defer.resolve(); })
.catch(function(e){ defer.reject(e); })
}});

suite
.on('cycle', function(event) {
console.log(String(event.target));
Expand Down
Loading