CLOVER🍀

That was when it all began.

HazelcastのJavaScriptクライアントを試す

Hazelcastに、JavaScriptでのクライアントができていたようです。

Getting Started with Hazelcast and Node.js | Hazelcast Blog

hazelcast-client - npm

GitHub - hazelcast/hazelcast-nodejs-client: Hazelcast IMDG Node.js Client

Hazelcastには、クライアント向けのバイナリプロトコルがドキュメントとして公開されていて、これをJavaScriptで実装したものみたいです。

Documentation - Hazelcast IMDG

http://docs.hazelcast.org/docs/HazelcastOpenBinaryClientProtocol-Version1.0-Final.pdf

http://docs.hazelcast.org/docs/ClientProtocolImplementationGuide-Version1.0-Final.pdf

実際、モジュールの説明としても

Node.js Client for Hazelcast, using Hazelcast Open Client Protocol 1.0 for Hazelcast 3.6 and higher

https://www.npmjs.com/package/hazelcast-client

と書かれていますしね。

プログラム自体は、TypeScriptで書かれているようです。このため、型定義などはとても見やすい感じになっています。
※このブログを書いている人は、TypeScriptはやったことありません

ただ、まだまだ開発途中なので、本格的に利用可能になるのはもっと先の話でしょう。

まあ、試しということで今の状態で1度使ってみましょう。Hazelcast JavaScriptクライアントのバージョンは、0.2.1を扱います。

準備

プログラムは、ECMAScript 2015(Babel)およびMocha、Chaiを使ってテストコードとして実装してみます。

npmモジュールのインストールは、こちらで。

$ npm init
$ npm install --save hazelcast-client
$ npm install --save-dev mocha chai babel-register babel-preset-es2015

依存関係は、このように。

  "dependencies": {
    "hazelcast-client": "^0.2.1"
  },
  "devDependencies": {
    "babel-preset-es2015": "^6.6.0",
    "babel-register": "^6.7.2",
    "chai": "^3.5.0",
    "mocha": "^2.4.5"
  }

.babelrcにpresetsとして、es2015を設定。
.babelrc

{
    "presets": ["es2015"]
}

また、package.jsonでは、テスト時にMocha+Babelで実行するようにします。

  "scripts": {
    "test": "mocha --compilers js:babel-register"
  },

テスト用ディレクトリも作成。

$ mkdir test

それでは、コードを書いていってみます。

HazelcastClientを使う

とりあえず、いきなり全体を載せてみます。
test/hazelcast-js-client-test.js

const HazelcastClient = require("hazelcast-client").Client;
const Q = require("q");
const should = require("chai").should();

describe("Hazelcast JavaScript Client", () => {
    it("Getting Started", () => {
        return HazelcastClient.newHazelcastClient().then(client => {
            console.log("connectd.");

            const map = client.getMap("defaultMap");

                // put
            return map.put("key1", "value1")

                // get and verify
                .then(() => map.get("key1"))
                .then(value => value.should.equal("value1"))

                // remove
                .then(() => map.remove("key1"))
                .then(() => map.get("key1"))
                .then(value => should.equal(value, null))

                // empty?
                .then(() => map.isEmpty())
                .then(result => result.should.true)

                // put objects
                .then(() => {
                    const entries = [
                        { key: "key100", value: { name: "磯野カツオ", age: 11 } },
                        { key: "key200", value: { name: "磯野ワカメ", age: 9 } },
                        { key: "key300", value: { name: "フグ田タラオ", age: 3 } }
                    ];

                    return Q.all(entries.map(e => map.put(e.key, e.value)));
                })

                // size
                .then(() => map.size())
                .then(size => size.should.equal(3))

                // object get
                .then(() => map.get("key200"))
                .then(person => person.should.deep.equal({ name: "磯野ワカメ", age: 9 }))

                // clear
                .then(() => map.clear())

                // disconnect
                .fin(() => {
                    console.log("disconnect.");
                    client.shutdown()
                });
        });
    });

    it("specified, target hosts", () => {
        const Config = require("hazelcast-client").Config;
        const config = new Config.ClientConfig();
        config.networkConfig.addresses = [{ host: "localhost", port: 5702 }];

        return HazelcastClient.newHazelcastClient(config).then(client => {
            console.log("connectd.");

            const map = client.getMap("defaultMap");

                // put
            return map.put("key1", "value1")

                // get and verify
                .then(() => map.get("key1"))
                .then(value => value.should.equal("value1"))

                // clear
                .then(() => map.clear())

                // disconnect
                .fin(() => {
                    console.log("disconnect.");
                    client.shutdown()
                });
        });
    });
});

順を追って、説明していきます。

まずは、必要なモジュールをrequire。HazelcastClientは、「hazelcast-client」をrequireしてから、Clientとして取得します。

const HazelcastClient = require("hazelcast-client").Client;
const Q = require("q");
const should = require("chai").should();

Qは必須ではありませんが、今回のサンプルの都合上使用します。

Q自体は、Promiseを扱うためのライブラリです。

kriskowal/q

GitHub - kriskowal/q: A promise library for JavaScript

HazelcastのJavaScriptクライアントは、Promiseを返してきますが、このQを使った結果が返ってきます。

Mochaでのdescribeとitは飛ばして、まずはデフォルト設定で接続。

        return HazelcastClient.newHazelcastClient().then(client => {
            console.log("connectd.");

ここでは、QでのPromiseが返ってきます。

そして、IMapを取得。

            const map = client.getMap("defaultMap");

こちらの設定もデフォルトです。

あとは、適当にデータ操作してみましょう。ほとんどの操作で、Q.Promiseが返ってきます。これらをthenでつなげて、最後にfinで接続を閉じるようにしています。
やっていること自体は、単純にput/getしてChaiアサーションしたり、エントリをクリアしているだけです。

                // put
            return map.put("key1", "value1")

                // get and verify
                .then(() => map.get("key1"))
                .then(value => value.should.equal("value1"))

                // remove
                .then(() => map.remove("key1"))
                .then(() => map.get("key1"))
                .then(value => should.equal(value, null))

                // empty?
                .then(() => map.isEmpty())
                .then(result => result.should.true)

                // put objects
                .then(() => {
                    const entries = [
                        { key: "key100", value: { name: "磯野カツオ", age: 11 } },
                        { key: "key200", value: { name: "磯野ワカメ", age: 9 } },
                        { key: "key300", value: { name: "フグ田タラオ", age: 3 } }
                    ];

                    return Q.all(entries.map(e => map.put(e.key, e.value)));
                })

                // size
                .then(() => map.size())
                .then(size => size.should.equal(3))

                // object get
                .then(() => map.get("key200"))
                .then(person => person.should.deep.equal({ name: "磯野ワカメ", age: 9 }))

                // clear
                .then(() => map.clear())

                // disconnect
                .fin(() => {
                    console.log("disconnect.");
                    client.shutdown()
                });


IMapで使える型は、TypeScriptの定義を見るとよいでしょう。
IMap.d.ts

import * as Q from 'q';
import { DistributedObject } from './DistributedObject';
export interface IMap<K, V> extends DistributedObject {
    containsKey(key: K): Q.Promise<boolean>;
    containsValue(value: V): Q.Promise<boolean>;
    put(key: K, value: V, ttl?: number): Q.Promise<V>;
    get(key: K): Q.Promise<V>;
    remove(key: K, value?: V): Q.Promise<V>;
    size(): Q.Promise<number>;
    clear(): Q.Promise<void>;
    isEmpty(): Q.Promise<boolean>;
}

https://github.com/hazelcast/hazelcast-nodejs-client/blob/v0.2.1/src/IMap.ts

putAllなどもなく、Javaでのクライアントと同じだけの機能はまだありません。

Collectionとしても、まだIMapとISetがあるだけとなります。

接続先を指定する

続いての例は、接続先を明示的に指定したものです。こちらの場合は、Configを使用します。Configは、「hazelcast-client」をrequireしてConfigとして取得します。

        const Config = require("hazelcast-client").Config;
        const config = new Config.ClientConfig();
        config.networkConfig.addresses = [{ host: "localhost", port: 5702 }];

Configはこんな感じの定義になっているので、プロパティを見て設定しましょう。
Config.d.ts

import Address = require('./Address');
export declare class GroupConfig {
    name: string;
    password: string;
}
export declare class SocketOptions {
}
export declare class ClientNetworkConfig {
    addresses: Address[];
    connectionAttemptLimit: number;
    connectionAttemptPeriod: number;
    connectionTimeout: number;
    redoOperation: boolean;
    smartRouting: boolean;
    socketOptions: SocketOptions;
    constructor();
}
export declare class SerializationConfig {
}
export declare class GlobalSerializerConfig {
}
export interface LifecycleListener {
    (event: string): void;
}
export declare class ListenerConfig {
    lifecycle: Function[];
    addLifecycleListener(listener: Function): void;
    getLifecycleListeners(): Function[];
}
export declare class ClientConfig {
    instanceName: string;
    properties: any;
    groupConfig: GroupConfig;
    networkConfig: ClientNetworkConfig;
    listeners: ListenerConfig;
    serializationConfig: SerializationConfig;
}

https://github.com/hazelcast/hazelcast-nodejs-client/blob/v0.2.1/src/Config.ts

動作確認

今回作成したのは、Hazelcastのクライアントになるので、サーバーが必要です。

サーバー側は、Groovyで超シンプルに書きました。
server.groovy

@Grab('com.hazelcast:hazelcast:3.6.2')
import com.hazelcast.core.Hazelcast

def hazelcast = Hazelcast.newHazelcastInstance()

System.console().readLine("> Enter, stop server.")

これで、2 Node起動。

# Node 1
$ groovy server.groovy

# Node 2
$ groovy server.groovy

クラスタが構成されますよ、と。

Members [2] {
	Member [172.17.0.1]:5701 this
	Member [172.17.0.1]:5702
}

では、動かしてみます。

$ npm run test

> hazelcast-javascript-client@1.0.0 test /xxxxx
> mocha --compilers js:babel-register



  Hazelcast JavaScript Client
[DefaultLogger] INFO at ClusterService: Members received.
[ Member {
    address: Address { host: '172.17.0.1', port: 5701 },
    uuid: 'fe8c7e27-7c84-4a74-be11-3a7d7726ca27',
    isLiteMember: false,
    attributes: {} },
  Member {
    address: Address { host: '172.17.0.1', port: 5702 },
    uuid: '503a9189-42df-403b-8b2d-9bd7de60d09d',
    isLiteMember: false,
    attributes: {} } ]
[DefaultLogger] INFO at HazelcastClient: Client started
connectd.
disconnect.
    &#10003; Getting Started (250ms)
[DefaultLogger] INFO at ClusterService: Members received.
[ Member {
    address: Address { host: '172.17.0.1', port: 5701 },
    uuid: 'fe8c7e27-7c84-4a74-be11-3a7d7726ca27',
    isLiteMember: false,
    attributes: {} },
  Member {
    address: Address { host: '172.17.0.1', port: 5702 },
    uuid: '503a9189-42df-403b-8b2d-9bd7de60d09d',
    isLiteMember: false,
    attributes: {} } ]
[DefaultLogger] INFO at HazelcastClient: Client started
connectd.
disconnect.
    &#10003; specified, target hosts (55ms)


  2 passing (318ms)

ちゃんと2つのNodeを、クライアントから認識しつつ動いたみたいです。それにしても、起動が早い…。

まとめ

HazelcastのJavaScriptクライアントを試してみました。まだまだ開発途中ですが、型定義も見ようと思えば見れるので、少しは扱いやすい?

今後の開発に期待しましょう。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/hazelcast-examples/tree/master/hazelcast-javascript-client