oleksiyk/no-riak

View on GitHub
README.md

Summary

Maintainability
Test Coverage
[![Build Status][badge-travis]][travis]
[![Test Coverage][badge-coverage]][coverage]
[![david Dependencies][badge-david-deps]][david-deps]
[![david Dev Dependencies][badge-david-dev-deps]][david-dev-deps]
[![license][badge-license]][license]

# no-riak

__no-riak__ is a [Basho Riak KV](http://basho.com/products/riak-kv/) client for Node.js with easy to use [wrappers over CRDT data types](#crdt-data-types).
Supports Riak [authentication](#authentication), [conection pooling and balancing](#connection-pooling-and-load-balancing) across multiple servers according to their weight.
All methods will return a [promise](https://github.com/petkaantonov/bluebird)

* [Installation](#installation)
* [Usage](#usage)
  * [Quick example](#quick-example)
  * [Key/Value operations](#keyvalue-operations)
  * [Secondary indexes](#secondary-indexes)
  * [Map/Reduce](#mapreduce)
  * [Operations on Buckets and Bucket Types](#operations-on-buckets-and-bucket-types)
* [Other operations](#other-operations)
* [Authentication](#authentication)
* [CRDT Data Types](#crdt-data-types)
  * [Counter](#counter)
  * [Set](#set)
  * [Map](#map)
* [Connection pooling and load balancing](#connection-pooling-and-load-balancing)
  * [Handling connection errors](#handling-connection-errors)

## Installation

```
npm install no-riak
```

## Usage

### Quick example

```javascript
var Riak  = require('no-riak');

var client = new Riak.Client();

return client.put({
    bucket: 'test-bucket',
    key: 'key1',
    content: {
        value: 'hello'
    }
})
.then(function () {
    return client.get({
        bucket: 'test-bucket',
        key: 'key1'
    });
})
.then(function (result) {
    // result => { content:
    //    [ { value: <Buffer 68 65 6c 6c 6f>,
    //        vtag: '7V2EHl2Wh06SCAIl4y7M2Y',
    //        last_mod: 1454584844,
    //        last_mod_usecs: 893098 } ],
    //   vclock: 'a85hYGBgzGDKBVI8ypz/fn5Ie3OPQeizegZTIlMeKwPPBvULfFkA' }

    console.log(result.content[0].value.toString()); // => 'hello'
});

```

### Key/Value operations

- `get(params)`
- `put(params)`
- `del(params)`
- `update(params)`
- `listKeys(params)`
- `updateCounter(params)`
- `getCounter(params)`

### Secondary indexes

- `index(params)`

Example:

```javascript
var bucket = 'no-riak-test-kv';

return Promise.all([0, 1, 2].map(function (i) {
    return client.put({
        bucket: bucket,
        key: 'key' + i,
        content: {
            value: 'i' + i,
            indexes: [{
                key: 'no-riak-test_bin',
                value: 'indexValue'
            }]
        }
    });
}))
.then(function () {
    return client.index({
        bucket: bucket,
        index: 'no-riak-test_bin',
        qtype: 0,
        max_results: 2,
        key: 'indexValue'
    });
})
.then(function (result) {
    // result => { results: [ 'key0', 'key1' ], continuation: 'g20AAAAEa2V5MQ==' }

    // now get rest of search results:
    return client.index({
        bucket: bucket,
        index: 'no-riak-test_bin',
        qtype: 0,
        continuation: result.continuation,
        key: 'indexValue'
    });
})
.then(function (result){
    // result => { results: [ 'key2' ] }
});
```

### Map/Reduce

- `mapReduce()`

Example:

```javascript
var bucket = 'no-riak-test-kv';
var keys = [];
return Promise.all([0, 1, 2].map(function (i) {
    keys[i] = 'mr_key' + i;
    return client.put({
        bucket: bucket,
        key: keys[i],
        content: {
            value: {
                num: i + 10
            }
        }
    });
}))
.then(function () {
    return client.mapReduce({
        request: {
            inputs: keys.map(function (k) { return [bucket, k]; }),
            query: [{
                map: { // this phase will return JSON decoded values for each input
                    source: 'function(v) { var d = Riak.mapValuesJson(v)[0]; return [d]; }',
                    language: 'javascript',
                    keep: true
                }
            }, { // this phase will return the `num` property of each value
                reduce: {
                    source: 'function(values) { return values.map(function(v){ return v.num; }); }',
                    language: 'javascript',
                    keep: true
                }
            }, { // this phase will return a sum of all values
                reduce: {
                    module: 'riak_kv_mapreduce',
                    function: 'reduce_sum',
                    language: 'erlang',
                    keep: true
                }
            }]
        }
    });
})
.then(function (results) {
    // [ [ { num: 10 }, { num: 12 }, { num: 11 } ], // thats phase 1 results
    //   [ 10, 12, 11 ], // phase 2 results
    //   [ 33 ] ] // phase 3 results

    // each index in results array is an array of results for each map/reduce phase
    // even if phase results were stripped with keep: false
});
```

### Operations on Buckets and Bucket Types

- `listBuckets()`
- `getBucket()`
- `setBucket()`
- `resetBucket()`
- `getBucketType()`
- `setBucketType()`

Example:

```javascript
return client.setBucket({
    bucket: 'some-bucket',
    props: {
        allow_mult: true,
        r: 'all' // possible string values are: one, quorum, all, default
    }
});
```

## Other operations

- `ping()`
- `getServerInfo()`

## Authentication

Enable authentication in Riak, create user, add corresponding grants, example:

```bash
riak-admin security enable
riak-admin security add-user test password=secret
riak-admin security grant riak_kv.put,riak_kv.get on any to test
riak-admin security add-source test 127.0.0.1/32 password
```

And then simply provide `auth` option when creating Client:

```javascript
var client = new Riak.Client({
    auth: {
        user: 'test',
        password: 'secret'
    }
});
```

All communication will be encrypted over TLS. You can override TLS options:

```javascript
var client = new Riak.Client({
    auth: {
        user: 'test',
        password: 'secret'
    },
    tls: {
        secureProtocol: 'SSLv23_method',
        rejectUnauthorized: false,
        ciphers: 'DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:AES128-SHA256:AES128-SHA:AES256-SHA256:AES256-SHA:RC4-SHA'
    }
});
```

## CRDT Data Types

You can operate on a lower level with [Riak CRDT Data Types](http://docs.basho.com/riak/latest/dev/using/data-types/) with the following methods:

- `dtFetch()`
- `dtUpdate()`

__no-riak__ also provides easy to use wrappers over Map, Set and Counter.

### Counter

Represents signed 64 bit integer (via [long.js](https://github.com/dcodeIO/long.js))

- `increment(value)` [sync] increment counter value with positive or negative value, returns `this`
- `key()` [sync] get counter key
- `value()` [sync] return counter value
- `load()` [async] load counter value from Riak and return `this` in a Promise
- `save()` [async] save counter to Riak and return `this` in a Promise

```javascript
var Riak = require('no-riak');
var client = new Riak.Client();

var bucket = 'no_riak_test_crdt_counter_bucket';
var bucketType = 'no_riak_test_crdt_counter';

var counter = new Riak.CRDT.Counter(client, {
    bucket: bucket,
    type: bucketType
});

return counter.increment(1).increment(-5).save().call('value')
.then(function (v) {
    // v.toNumber() => -4
});
```

### Set

Represents an array of uniqe opaque Buffer values.

- `key()` [sync] get set key
- `value()` [sync] return set value
- `load()` [async] load set value from Riak and return `this` in a Promise
- `save()` [async] save set to Riak and return `this` in a Promise
- `add(value)` [sync] add new value to set, returns `this`
- `remove(value)` [sync] removes value from the set, returns `this`

Example:

```javascript
var bucket = 'no_riak_test_crdt_set_bucket';
var bucketType = 'no_riak_test_crdt_set';

var set = new Riak.CRDT.Set(client, {
    bucket: bucket,
    type: bucketType
});

return set
    .add('a1', 'a2', 'a3', 'a2', 'a2', 'a3')
    .remove('a1')
    .save()
    .call('value')
    .then(function (v) {
        // v => ['a2', 'a3']
    });
```

By default __no-riak__ will convert set values to strings, if you want to stick with buffers, pass `strings: false` option to Set constructor:

```javascript
var set = new Riak.CRDT.Set(client, {
    bucket: bucket,
    type: bucketType,
    strings: false
});
```

### Map

Represents a list of name/value pairs. Values can be Counters, Sets, Maps, Registers and Flags.

- `key()` [sync] get map key
- `value()` [sync] return map value
- `load()` [async] load map value from Riak and return `this` in a Promise
- `save()` [async] save map to Riak and return `this` in a Promise
- `update(name, value)` [sync] update existing field or add new field to map, returns `this`
- `remove(name, type)` [sync] remove existing field from the map. `constructor` is one of the following: `Riak.CRDT.Counter`, `Riak.CRDT.Set`, `Riak.CRDT.Map`, `Riak.CRDT.Map.Register`, `Riak.CRDT.Map.Flag`
- `get(name)` [sync] get Riak.CRDT.* instance for corresponding field `name`. This instance can be used to update the map.

Example:

```javascript
var bucket = 'no_riak_test_crdt_map_bucket';
var bucketType = 'no_riak_test_crdt_map';

var map = new Riak.CRDT.Map(client, {
    bucket: bucket,
    type: bucketType
});

return map
    .update('key1', new Riak.CRDT.Counter().increment(-5))
    .update('key2', new Riak.CRDT.Set().add('a1', 'a2', 'a3').remove('a2'))
    .save()
    .call('value')
    .then(function (v) {
        console.log(v); // => { key1: { low: -5, high: -1, unsigned: false }, key2: [ 'a1', 'a3' ] }
    });
```

Using `get(name)` to operate on map fields

```javascript
var set;
var map = new Riak.CRDT.Map(client, {
    bucket: bucket,
    type: bucketType
});

map
    .update('key1', new Riak.CRDT.Counter().increment(-5))
    .update('key2', new Riak.CRDT.Set().add('a1', 'a2', 'a3'));

set = map.get('key2');
set.remove('a2');

map.save().call('value').then(function (v){
    console.log(v); // => { key1: { low: -5, high: -1, unsigned: false }, key2: [ 'a1', 'a3' ] }
});
```

Using Map.register and Map.Flag:

```javascript
var map = new Riak.CRDT.Map(client, {
    bucket: bucket,
    type: bucketType
});

map
    .update('key1', new Riak.CRDT.Map.Register().set('a1'))
    .update('key2', new Riak.CRDT.Map.Flag().enable())
    .save()
    .call('value')
    .then(function (v){
        console.log(v); // { key1: 'a1', key2: true }
    });
```

Set and Register values in a Map will be by default converted to strings, pass `strings: false` to Map constructor to receive buffers instead:

```javascript
var map = new Riak.CRDT.Map(client, {
    bucket: bucket,
    type: bucketType,
    strings: false
});
```

## Connection pooling and load balancing

__no-riak__ can balance requests across a pool of connections. It can also fill the connections pool according to the list of servers and their corresponding weight:

```javascript
var client = new Riak.Client({
    connectionString: '10.0.1.1:8087:4,10.0.1.2:8087:3,10.0.1.3:8087:2',
    pool: {
        min: 9
    }
});
```

Here, 9 connections will be created when client starts, 4 of which will connect to '10.0.1.1', 3 to '10.0.1.2' and 2 to '10.0.1.3'. When there is a demand for more connections, no-riak will create up to `pool.max` connections and will also split them across servers considering their weight.

### Handling connection errors

__no-riak__ will retry any request that failed due to network (connection) error, the corresponding option is
* `retries` - number of retries for each failed request, defaults to 3

__no-riak__ can temporary remove the server whose connections are failing with some kind of network error (socket timeout, connection refused, etc).
Such server will be assigned effective weight=0 and and so its connections will be replaced by connections to other servers in the cluster.
__no-riak__ will then periodically check disabled servers and restore them with their original weight once they are back online.
Two options help control this behaviour:

* `maxConnectionErrors` - maximum number of connections errors for the server, defaults to 3
* `maxConnectionErrorsPeriod` - period in ms which is considered when counting number of errors, defaults to 60000 (1 min)

Default options mean that if any server had 3 or more errors within last minute then this server is marked as down.

__no-riak__ will emit two events which can be useful to track disabled servers:

* `net:hostdown` - emitted when host has reached configured error rate and is now temporary disabled
* `net:hostup` - emitted when host is ready to accept new connections and will now take part in load balancing

You can also query current connections pool state like this:

```javascript
var stats = client.pool.count();
// => { free: { '10.0.1.5:8087': 5, '10.0.1.3:8087': 5, '10.0.1.4:8087': 5, '10.0.1.1:8087': 1, '10.0.1.2:8087': 1 }, busy: {} }
```

[badge-license]: https://img.shields.io/badge/License-MIT-green.svg
[license]: https://github.com/oleksiyk/no-riak/blob/master/LICENSE
[badge-travis]: https://api.travis-ci.org/oleksiyk/no-riak.svg?branch=master
[travis]: https://travis-ci.org/oleksiyk/no-riak
[badge-coverage]: https://codeclimate.com/github/oleksiyk/no-riak/badges/coverage.svg
[coverage]: https://codeclimate.com/github/oleksiyk/no-riak/coverage
[badge-david-deps]: https://david-dm.org/oleksiyk/no-riak.svg
[david-deps]: https://david-dm.org/oleksiyk/no-riak
[badge-david-dev-deps]: https://david-dm.org/oleksiyk/no-riak/dev-status.svg
[david-dev-deps]: https://david-dm.org/oleksiyk/no-riak#info=devDependencies
[badge-bithound-code]: https://www.bithound.io/github/oleksiyk/no-riak/badges/code.svg
[bithound-code]: https://www.bithound.io/github/oleksiyk/no-riak
[badge-bithound-overall]: https://www.bithound.io/github/oleksiyk/no-riak/badges/score.svg
[bithound-overall]: https://www.bithound.io/github/oleksiyk/no-riak
[badge-bithound-deps]: https://www.bithound.io/github/oleksiyk/no-riak/badges/dependencies.svg
[bithound-deps]: https://www.bithound.io/github/oleksiyk/no-riak/master/dependencies/npm
[badge-bithound-dev-deps]: https://www.bithound.io/github/oleksiyk/no-riak/badges/devDependencies.svg
[bithound-dev-deps]: https://www.bithound.io/github/oleksiyk/no-riak/master/dependencies/npm