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']",)})
原因はよく分からないが、ここを参考に、回避
docker上とcqlshのバージョンは同じなのだから、ええ案配にサーバーとCQLバージョンくらいネゴってほしい。
cqlshの設定ファイルに記載することでバージョンは指定しなくてもよいようだ。ここを参考に ~/.cassandra/cqlshrc を作成する。
最初、 ~/.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
{
"name": "cassandra",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "./node_modules/.bin/tsc -w -d --sourceMap"
}
}
- 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
キースペース・テーブルの作成、および、データ投入用スクリプト
キースペース・テーブルの作成、および、データ投入スクリプト
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 には、ここにあるように、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にアクセスするクライアントモジュール
// このソースをコンパイルするには、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を実行
//
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();
});
実行結果
$ 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 } ]