node.js からDocker上のCassandraにアクセスする

# node.js からDocker上のCassandraにアクセスする。

KVSの一つとして、Cassandraというのがあると聞いて、node.jsからtypescriptアプリを作成し、Cassandoraにアクセスしてみる。
そのときの谷あり、谷ありの苦労を忘れないためのメモ


## 環境
---

 * Cassandra環境(3.10) : Docker on Linux(CentOS7)上に構築
 * Node環境(v8.1.2) : OSX 10.11.6 上に構築


## Cassandraインストール
---

使っているバージョンが分からないのが嫌いなのでTAGを指定してLinux上に構築したDocker環境にインストール
時間があれば、Cassandraのクラスタ構成をチャレンジしてみよう。

```
$ docker pull cassandra:3.10
$ docker volume create --name main-cassandra-db
$ docker run -d --name main-cassandra -v main-cassandra-db:/var/lib/cassandra -p 9160:9160 -p 9042:9042 -m 1G cassandra:3.10
```

Linux上には、cqlshをインストールしていないので、Docker上のcqlshを使用してアクセス。立ち上がっていることを確認。

```
$ docker exec -it main-cassandra cqlsh
Connected to Test Cluster at 127.0.0.1:9042.
[cqlsh 5.0.1 | Cassandra 3.10 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh>
```

ただ、keyspaceなどをバッチ操作するのに、OSXからアクセスできた方が便利なので、OSX側にはcqlshをインストールする。
cqlshは、pythonで作られているので、pipコマンドでインストール。

```
$ python --version
Python 2.7.12
$ pip install cqlsh
$ cqlsh --version
cqlsh 5.0.1
```

ここで、最初の谷が。
OSXからLinux上のCassandraにアクセスしようとすると失敗する。(192.168.45.131はLinuxのIPアドレス)

```
$ cqlsh 192.168.45.131
Connection error: ('Unable to connect to any servers', {'192.168.45.131': ProtocolError("cql_version '3.3.1' is not supported by remote (w/ native protocol). Supported versions: [u'3.4.4']",)})
```

~~原因はよく分からないが、[ここ](https://stackoverflow.com/questions/41142540/cassandra-version-error)を参考に、回避
docker上とcqlshのバージョンは同じなのだから、ええ案配にサーバーとCQLバージョンくらいネゴってほしい。~~

cqlshの設定ファイルに記載することでバージョンは指定しなくてもよいようだ。[ここ](http://docs.datastax.com/en/cql/3.3/cql/cql_reference/cqlshUsingCqlshrc.html)を参考に ~/.cassandra/cqlshrc を作成する。
最初、 ~/.cqlshrc を作成していたのだが、 ~/.cassandra/cqlshrc が無いと ~/.cassandra/cqlshrc にリネイムされた。

```:~/.cassandra/cqlshrc
[cql]
version = 3.4.4
```

```
$ cqlsh 192.168.45.131
Connected to Test Cluster at 192.168.45.131:9042.
[cqlsh 5.0.1 | Cassandra 3.10 | CQL spec 3.4.4 | Native protocol v4]
Use HELP for help.
cqlsh>
```

試しに、Pythonのバージョンを3.5.2に上げてみる。が、そもそも動かない。
~~調べれば分かることだが、きっと Python2.7しかサポートしていないのでは?~~
cqlshのソースを確認したところ、きっちりPythonのバージョンをチェックしていた。

```
if sys.version_info[:2] != (2, 7):
    sys.exit("\nCQL Shell supports only Python 2.7\n")
```

## Node.jsのインストール
---

Node.jsをインストールする。4月にNode.jsで遊んでいた頃はv6だったが、最近v8が出たようなのでv8で試してみる。

```
$ nodebrew install-binary v8.1.2
$ nodebrew use v8.1.2
use v8.1.2
$ node --version
v8.1.2
```

TypescriptでCassandraのクライアントを作るので、必要な定義ファイルを作成する。

 * package.json

```json:package.json
{
  "name": "cassandra",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "./node_modules/.bin/tsc -w -d --sourceMap"
  }
}
```

 * tsconfig.json

```json:tsconfig.json
{
  "compilerOptions": {
    /* Basic Options */
    "target": "es2015",                /* Specify ECMAScript target version. */
    "module": "commonjs",              /* Specify module code generation. */
    "declaration": true,               /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                 /* Generates corresponding '.map' file. */

    /* Strict Type-Checking Options */
    "strict": true                     /* Enable all strict type-checking options. */
  },

  "exclude": [
   "node_modules"
  ]
}
```

必要なパッケージをインストール

```
$ npm install --save-dev typescript
$ npm install --save cassandra-driver
$ npm install --save-dev @types/cassandra-driver
```


## キースペース・テーブルの作成、および、データ投入用スクリプト
---

キースペース・テーブルの作成、および、データ投入スクリプト

```cql:sample.cql
create keyspace IF NOT EXISTS Sample
       with replication = {'class':'SimpleStrategy','replication_factor':1};

create table IF NOT EXISTS Sample.Cookie (
  id text primary key,     // Primary key SHA256 hash
  cookie text,             // Cookie Text
  update_time timestamp,   // Update Time
  create_time timestamp    // Create Time
) WITH comment='Cookie management table.';

begin batch

insert into Sample.Cookie ( id, create_time ) VALUES ( '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', toTimestamp(now()) );
insert into Sample.Cookie ( id, create_time ) VALUES ( 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35', toTimestamp(now()) );

apply batch;
```

## Cassandraへのデータ投入
---

先に作成したスクリプトを使用して、Docker上に構築したCassandraに対して、キースペース、テーブル、データ投入を行う。

```
$ cqlsh 192.168.45.131 < sample.cql
```

~~-f オプションでスクリプトを指定すると、データ投入スクリプトに書いたコメント「// Cookie Text」やcommentプロパティに日本語を使用すると以下のエラーメッセージが出る。
どうやら create tableから最後のコロンまでの間に日本語が存在すると駄目なようだ。~~

```
sample.cql:11:'ascii' codec can't encode characters in position 134-137: ordinal not in range(128)
sample.cql:26:InvalidRequest: Error from server: code=2200 [Invalid query] message="unconfigured table cookie"
```

しかし、docker中のcqlshを使用するとエラーが起きることなくテーブルの作成できる。Pythonのバージョンの違い??
ではないようだ、OSXに2.7.9をインストールしてみたが、エラーは変わらなかった。

```
$ docker exec -it main-cassandra bash
root@32e0a9c14cc7:/# cqlsh -f /tmp/sample.cql
root@32e0a9c14cc7:/# python --version
Python 2.7.9
```


## node.jsからTypescriptアプリを使用してCassandraへアクセスする。
---

cassandra-driver には、[ここ](http://docs.datastax.com/en/developer/nodejs-driver/3.2/api/class.Client/)にあるように、Promiseスタイルか、コールバック関数スタイルのどちらかで利用する必要がある。ここは時代に合わせてPromiseスタイルで行くことにした。
しかし、npmに登録されている @types/cassandra-driver は、コールバック関数スタイルしか方定義されていない。そのため、Promiseスタイルで記述すると以下のようにエラーとなる。

```
$ npm start
> tsc -w -d --sourceMap
src/client.ts(17,44): error TS2339: Property 'then' does not exist on type 'void'.
src/client.ts(24,52): error TS2339: Property 'then' does not exist on type 'void'.
```

githubに、Promiseスタイルの方定義をしたファイルが存在するので、上書きすることでPromiseスタイルのtypescriptソースがコンパイルできるようになる。

```
$ wget https://raw.githubusercontent.com/aliem/DefinitelyTyped/feature/cassandra-driver-promises/cassandra-driver/index.d.ts \
         -O node_modules/@types/cassandra-driver/index.d.ts
```

 * client.ts Cassandraにアクセスするクライアントモジュール

```typescript:client.ts
// このソースをコンパイルするには、npm --save-dev @types/cassandra-driver を実行した後、
// node_modules/@types/cassandra-driver/index.d.ts を以下のファイルと置き換える必要がある。
// master には、callback形式の型は登録されているが、Promise形式の型が登録されていないため、コンパイルエラーとなる。
// なぜか、masterにマージされていない。おそらく、lintエラー取るためにかなり手を入れているようだから、その影響かもしれない。
// wget https://raw.githubusercontent.com/aliem/DefinitelyTyped/feature/cassandra-driver-promises/cassandra-driver/index.d.ts \
//         -O node_modules/@types/cassandra-driver/index.d.ts
//
import {types, Client, QueryOptions} from 'cassandra-driver';

export class client extends Client {
    constructor(hosts: string) {
        super({ contactPoints: [ hosts ] });
    }

    public select(id: string): Promise<any> {
        const query: string = "select * from Sample.Cookie where id = ?";
        return this.execute(query, [ id ]).then((result: types.ResultSet) => {
            return (result.rows);
        });
    }

    public update(id: string, cookie: string ): Promise<any> {
        const query: string = "update Sample.Cookie set cookie = ?, update_time = toTimestamp(now()) where id = ? IF EXISTS";
        return this.execute(query, [ cookie, id ]).then((result: types.ResultSet) => {
            return (result);
        });
    }
}
```

 * main.ts クライアントモジュールを使用して、SELECTとUPDATEを実行

```typescript:main.ts
//
import {client} from './client';

var c: client;

new Promise((resolve) => {
    c = new client(process.argv[2]);
    resolve();
}).then(async () => {
    const result = await c.select('6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b');
    console.log(result);
}).then(async () => {
    const result = await c.update('6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', 'cookie string');
    if (result.rows && result.rows[0]['[applied]']) {
        console.log('更新成功');
    } else {
        console.log('更新失敗');
    }
    console.log(result);
}).then(async () => {
    const result = await c.select('6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b');
    console.log(result);
}).then(async () => {
    await c.shutdown();
}, async (err) => {
    console.log(err);
    await c.shutdown();
});
```


## 実行結果
---

```shell
$ node src/main.js 192.168.45.131
[ Row {
    id: '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b',
    cookie: 'cookie string',
    create_time: 2017-06-25T11:32:43.529Z,
    update_time: 2017-06-25T11:41:40.859Z } ]
更新成功
ResultSet {
  info: 
   { queriedHost: '192.168.45.131:9042',
     triedHosts: {},
     achievedConsistency: 10,
     traceId: undefined,
     warnings: undefined,
     customPayload: undefined },
  rows: [ Row { '[applied]': true } ],
  rowLength: 1,
  columns: [ { name: '[applied]', type: [Object] } ],
  pageState: null,
  nextPage: undefined }
[ Row {
    id: '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b',
    cookie: 'cookie string',
    create_time: 2017-06-25T11:32:43.529Z,
    update_time: 2017-06-26T10:37:42.001Z } ]
```