2011年2月27日日曜日

Trying thrift4go with Cassandra

2010/3/26追記:Goのrelease.2011-03-07.1とThriftのr1085676だとコンパイル通らなくなってる。たぶんGoの文法が(また)変わったのが原因。

Go用にコードを生成するthrift4goがThrift本体に取り込まれたので、早速それをCassandraとの連携で試してみることにする。試したのはThriftのr1075010。

Thriftのビルド

何はともあれThriftをビルドしないことにははじまらないので、SVNからソースを落としてきてビルドする。

$ svn co http://svn.apache.org/repos/asf/thrift/trunk thrift
$ cd thrift
$ ./bootstrap.sh
$ ./configure --with-go --with-cpp --without-java --without-php --without-python --without-erlang
$ make -j2
$ sudo make install
$ cd lib/go
$ make && make install

注意点としては、なぜか単純にconfigure -> make -> make installしただけではGo用のライブラリがビルドされないので、手動でGoのライブラリをmake installしないといけないこと。これは単にconfigureスクリプトまわりのバグか、自分のやり方が悪いかのどっちかだけどとりあえず放置して先に進む。

ちなみに、Goのライブラリをインストールすると$GOROOT/pkg/linux_386/といった場所(このパスは環境に依存する)にthrift.aという名前でライブラリのバイナリが配置される。ここに配置されることによって、Goのプログラムから'import "thrift"'という感じでパッケージのインポートができるようになるけど、もしこのパスに置かずに直接インポートしようと思ったらthrift.aへのパスを明示的に指定しないといけない。

  • $GOROOT/pkg/linux_386/にthrift.aがある場合
  • package main
    import (
        "thrift"
    )
    ...
    
  • $GOROOT/pkg/linux_386/以外の場所(例えばプログラムと同じディレクトリ)にthrift.aがある場合
  • package main
    import (
        "./thrift"
    )
    ...
    

ThriftによるCassandraコードの生成

Thriftのビルドとインストールは終わったので、今度はCassandraのインタフェース定義からGoのコードを生成してもらって、コンパイルする。

$ thrift --gen go ~/lib/cassandra/interface/cassandra.thrift
$ cd gen-go/cassandra/
$ make
8g -o _go_.8 ttypes.go Cassandra.go
Cassandra.go:89: syntax error: unexpected range, expecting )
Cassandra.go:99: syntax error: unexpected name, expecting )
Cassandra.go:109: syntax error: unexpected name, expecting )
Cassandra.go:121: syntax error: unexpected name, expecting )
...

とここでコンパイルエラー。ソースを見てみたら、どうやら変数名に予約語のrangeが使用されてるせいでコンパイルエラーが起きている様子。ということでcassandra.thriftの定義を少々いじる。

$ diff cassandra.thrift cassandra.thrift.org
421c421
<                                   3:required KeyRange key_range,
---
>                                   3:required KeyRange range,

そこから改めてコード生成してコンパイルしたら上手くいった。

$ thrift --gen go ~/lib/cassandra/interface/cassandra.thrift
$ cd gen-go/cassandra/
$ make
8g -o _go_.8 ttypes.go Cassandra.go
rm -f _obj/thriftlib/cassandra.a
gopack grc _obj/thriftlib/cassandra.a _go_.8
cp _obj/thriftlib/cassandra.a "/home/masato/lib/google-go/pkg/linux_386/thriftlib/cassandra.a"
$
(続く)

2011年2月26日土曜日

Go + SWIG + Cassandra + Thrift (5)

つい最近thrift4goなんてのがThrift本体に取り込まれたらしい。なので、あえてC++で書いものをSWIGでラップする必要性は薄れたかもしれないけど、とりあえず最後まで書いていく。

さて、前回まででSWIG用のインタフェース定義を書き終わったので今度はそれを実際にビルドして、Goから使うところまでをやっていく。

SWIGでのビルド方法

基本的には公式に書かれている手順に従えばそのままビルドできる。ただ手順が複雑なので、手でやるのはあまり現実的でない。Makefileなりを書くのがいいと思う。ちなみに自分はSConsやらCMakeを使ったビルドもちょっと挑戦したけど、SWIGとのうまい連携が難しそうだったのでやっぱりMakefileで力技に落ちついた。

Makefile

簡単な流れを説明すると以下の通り。Makefileはあまり書き慣れてない(そもそもあまり好きでない)ので、そんなに参考にしないほうがいいかも。

  1. 自分で書いたラッパーをコンパイル。
  2. SWIGにラッパーコードを生成させてコンパイル。
  3. コンパイルしたものを共有ライブラリにまとめる。
  4. SWIGが生成したGoパッケージ用のコードをコンパイルしてパッケージ化。
PACKAGE_NAME=cassandra
SRC=Client.cpp CassandraAPI.cpp
OBJ=$(SRC:%.cpp=%.o)

THRIFT_SRC=Cassandra.cpp cassandra_constants.cpp cassandra_types.cpp
THRIFT_DIR=./gen-cpp
THRIFT_OBJ=$(THRIFT_SRC:%.cpp=%.o)

SWIG_OBJ=$(PACKAGE_NAME)_wrap.o
SWIG_SRC=$(PACKAGE_NAME)_wrap.cxx
CC=g++
LIBS=-lthrift

# i686
GO_ARCH=8

vpath %.cpp $(THRIFT_DIR)

all: lib golib

# C++ lib with SWIG
lib: $(OBJ) $(SWIG_OBJ) $(THRIFT_OBJ)
        $(CC) -shared $^ $(LIBS) -o $(PACKAGE_NAME).so
        cp $(PACKAGE_NAME).so ..

swig_interface: $(PACKAGE_NAME).i
        swig -Wall -c++ -go -I/usr/local/include/thrift $<

$(SWIG_OBJ): swig_interface $(SWIG_SRC)
        $(CC) -Wall -g -c -fpic -I/usr/local/include/thrift/ -I$(THRIFT_DIR) $(SWIG_SRC)

.cpp.o:
        $(CC) -Wall -g -c -fpic -I/usr/local/include/thrift/ -I$(THRIFT_DIR) $<

# Go lib
golib: $(PACKAGE_NAME).$(GO_ARCH) $(PACKAGE_NAME)_gc.$(GO_ARCH)
        gopack grc $(PACKAGE_NAME).a $^

$(PACKAGE_NAME)_gc.$(GO_ARCH):$(PACKAGE_NAME)_gc.c
        $(GO_ARCH)c -I $(GOROOT)/pkg/linux_386/ $<

$(PACKAGE_NAME).$(GO_ARCH): $(PACKAGE_NAME).go
        $(GO_ARCH)g $<

clean:
        rm -f *.o *.so *.8 *.a $(SWIG_SRC) $(PACKAGE_NAME)_gc.c $(PACKAGE_NAME).go

ビルド出力

SWIG実行時に大量に表示されるワーニングに関しては扱いが難しいところで、正しく直そうと思ったらThriftが生成したコードを手で修正しないといけない。自動生成されたコードを更に手で修正するのはあまり効率がいいとは思えないので、ひとまず放置するのが良い。

$ make
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp Client.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp CassandraAPI.cpp
swig -Wall -c++ -go -I/usr/local/include/thrift cassandra.i
gen-cpp/cassandra_types.h:26: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
gen-cpp/cassandra_types.h:38: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
gen-cpp/cassandra_types.h:46: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
/usr/local/include/thrift/Thrift.h:56: Warning 503: Can't wrap 'operator ++' unless renamed to a valid identifier.
/usr/local/include/thrift/Thrift.h:61: Warning 503: Can't wrap 'operator !=' unless renamed to a valid identifier.
/usr/local/include/thrift/Thrift.h:84: Warning 503: Can't wrap 'operator ()' unless renamed to a valid identifier.
gen-cpp/cassandra_types.h:59: Warning 451: Setting a const char * variable may leak memory.
(以下同様なwarningが数十行)
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp cassandra_wrap.cxx
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/Cassandra.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/cassandra_constants.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/cassandra_types.cpp
g++ -shared Client.o CassandraAPI.o cassandra_wrap.o Cassandra.o cassandra_constants.o cassandra_types.o -lthrift -o cassandra.so
cp cassandra.so ..
8g cassandra.go
8c -I /home/masato/lib/google-go/pkg/linux_386/ cassandra_gc.c
gopack grc cassandra.a cassandra.8 cassandra_gc.8

ラップしたライブラリをGoから呼ぶ

ここへ来てやってGoのコードが書ける。注意点としては、SWIGは特定のルールに基いて関数や型の名前をつけるので、良くわからない場合はSWIGが生成したコード、特にcassandra.goを見てGoからどうやって呼べばいいかを見る必要があるという点。

具体的に言うと、例えばラップしたクラスのなかにColumnPathというクラスがあって、それをGo側で生成したいと思った場合はcassandra.NewColumnPath()という関数を呼ばないといけないとか、ColumnPathクラスでpublicだったnameという変数にアクセスしようと思ったらGetName()でアクセスしないといけないだとか、そういう細かな話。この辺のルールはおそらく言語によって異なるうえ、SWIGのドキュメントにもあまり記述されてないっぽいので、生成されたコードを見た方がてっとり早い。

Goのサンプルコード

package main

import(
    "os"
    "log"
    "./lib/cassandra"
)

var logger *log.Logger = log.New(os.Stdout, "", 0)

func main() {
    defer func(){
        if err := recover(); err != nil {
            logger.Println("Error:",err)
        }
    }()

    client := cassandra.CreateClient("localhost", 9160)

    logger.Println("created client.")
    defer func(){
        cassandra.DestroyClient(client)
    }()

    logger.Printf("version: %s\n",client.GetVersion());

    keySpace := "sample_keyspace"
    client.SetKeySpace(keySpace)

    columnFamilyName := "SampleColumnFamily";
    columnName := "SampleName";
    path := cassandra.NewColumnPath()
    path.SetColumn_family(columnFamilyName)
    path.SetColumn(columnName)
    columnPathIsSet := cassandra.NewX_ColumnPath__isset()
    columnPathIsSet.SetColumn(true)
    path.SetX__isset(columnPathIsSet)
    key := "SampleColumn"
    column := cassandra.NewColumnOrSuperColumn()
    client.Get(column, key, path, cassandra.KOne)
    col := column.GetColumn()
    logger.Println("name:",col.GetName())
    logger.Println("value:",col.GetValue())
    logger.Println("timestamp:",col.GetTimestamp())
    logger.Println("TTL:",col.GetTtl())
}

出力

created client.
version: 19.4.0
name: SampleName
value: SampleValue
timestamp: 1297591990231000
TTL: 0

ちなみにわざと例外を発生させた場合は以下のような出力になる。見てわかるように、例外が発生した理由までちゃんと持ってこれている。これはSWIGのインタフェース定義に適切な例外処理を記述したおかげ。

$ ./sample
created client.
version: 19.4.0
Error: org::apache::cassandra::InvalidRequestException: Keyspace keyspaceThatDoesntExist does not exist

まとめ

以上、数回に渡ってGoとCassandraを連携させる方法を書いてきた。見てわかるように、現時点では非常に手順が多くて面倒なところが多い。特にSWIGはかなり仕様が膨大な上に資料も少ないので色々苦労すると思う。というかした。しかしSWIG自体はなにかと便利なので、覚えておくといつか役に立つ日がくるはず。

2011年2月20日日曜日

Go + SWIG + Cassandra + Thrift (4)

前回まででCassandraに入れたデータをC++で取り出せるようになった。今度はいよいよSWIGでC++をラップして、Goから呼ぶところをやっていく。

ラッパーの最上位APIを作成

ラップしたThriftそのものをGoから直接使用してもいいけど、今回はThriftの共通処理をC++側でラップしてしまって、単純な独自APIだけをGo側に公開していく方向にする。この方が(1)C++側で決まりきった処理を隠蔽できる、(2)色々泥臭いことも(やろうと思えば)C++側でできる、(3)自分で必要なものだけをGo側に公開することでGo側のコードが単純になる、といったメリットがある。

APIのざっくりとした設計

ライブラリのAPIはそれこそ色々考えられるけど、今回は以下のようなAPIを想定する。

package main
import (
    "cassandra"
)

fun main() {
    connectionA := cassandra.CreateClient("serverAtSomePlace.com", 6190)
    connectionB := cassandra.CreateClient("serverAtAnotherPlace.com", 6190)

    defer func() {
        // explicitly close connection before we exit
        cassandra.DestroyClient(connctionA)
        cassandra.DestroyClient(connectionB)
    }()

    awesomeData := connectionA.Get(/* some argument comes here*/)
    
    // process awesome data...
}

上の疑似コードで留意したのは主に以下の点。

  • ライブラリ内部で状態を持たない。状態を持つ必要があるもの(例えばネットワークの接続)は、APIによって作成されたオブジェクト内部に隠蔽して管理する。上の例で言うと、connectionAやconnectionBの中に接続状態が隠蔽されている。こうすると複数の接続を同時に管理できるし、初期化されてないライブラリ呼んじゃダメ、という制限が無くなる。

ラッパーのサンプルコード

以下が上の方針で書かれたサンプルコードである。

  • CassandraAPI.hpp
  • #ifndef CASSANDRA_CASSANDRAAPI_HPP
    #define CASSANDRA_CASSANDRAAPI_HPP
    
    #include <string>
    
    namespace cassandrawrap {
    
    class Client;
    
    Client* CreateClient(std::string, int);
    void DestroyClient(Client*);
    
    } // namespace
    
    #endif // CASSANDRA_CASSANDRAAPI_HPP
    
  • CassandraAPI.cpp
  • #include "CassandraAPI.hpp"
    #include "Client.hpp"
    using std::string;
    using cassandrawrap::Client;
    
    Client* cassandrawrap::CreateClient(std::string host, int port) {
        Client* client =  new Client();
        client->Open(host, port);
        return client;
    }
    
    void cassandrawrap::DestroyClient(Client* client) {
        if (client == NULL) {
            return;
        }
        client->Close();
        delete client;
    }
    
  • Client.hpp
  • #ifndef CASSANDRA_CLIENT_HPP
    #define CASSANDRA_CLIENT_HPP
    
    #include <string>
    #include <boost/shared_ptr.hpp>
    #include "ConsistencyLevel.hpp"
    #include "cassandra_types.h"
    
    namespace org { namespace apache { namespace cassandra {
    class CassandraClient;
    class ColumnOrSuperColumn;
    class ColumnPath;
    }}}
    
    namespace apache { namespace thrift { namespace protocol {
    class TProtocol;
    }}}
    
    namespace apache { namespace thrift { namespace transport {
    class TTransport;
    }}}
    
    
    namespace cassandrawrap {
    
    class Client {
    public:
        void Open(const std::string &, int);
        void Close();
        std::string GetVersion() const;
        void SetKeySpace(const std::string &);
        void Get(org::apache::cassandra::ColumnOrSuperColumn &,
                 const std::string &,
                 const org::apache::cassandra::ColumnPath &,
                 cassandrawrap::ConsistencyLevel) const;
    private:
        typedef boost::shared_ptr<apache::thrift::transport::TTransport> TTransportPtr;
        typedef boost::shared_ptr<apache::thrift::protocol::TProtocol> TProtocolPtr;
        typedef boost::shared_ptr<org::apache::cassandra::CassandraClient> CassandraClientPtr;
        TTransportPtr m_socket;
        TTransportPtr m_transport;
        TProtocolPtr m_protocol;
        CassandraClientPtr m_client;
    };
    
    } // namespace
    
    #endif // CASSANDRA_CONTEXT_HPP
    
  • Client.cpp
  • #include <protocol/TBinaryProtocol.h>
    #include <transport/TSocket.h>
    #include <transport/TTransportUtils.h>
    
    // auto-generated thrift interface code using cassandra.thrift in cassandra repository
    #include "Cassandra.h"
    #include "Client.hpp"
    
    using apache::thrift::protocol::TProtocol;
    using apache::thrift::protocol::TBinaryProtocol;
    using apache::thrift::transport::TSocket;
    using apache::thrift::transport::TTransport;
    //using apache::thrift::transport::TBufferedTransport;
    using apache::thrift::transport::TFramedTransport;
    using org::apache::cassandra::CassandraClient;
    using org::apache::cassandra::ColumnOrSuperColumn;
    using org::apache::cassandra::Column;
    using org::apache::cassandra::ColumnPath;
    using org::apache::cassandra::ConsistencyLevel;
    using org::apache::cassandra::KsDef;
    using org::apache::cassandra::InvalidRequestException;
    using org::apache::cassandra::NotFoundException;
    using apache::thrift::TException;
    using std::string;
    using cassandrawrap::Client;
    
    void Client::Open(const string &address, int port) {
        m_socket = TTransportPtr(new TSocket(address.c_str(), port));
        m_transport = TTransportPtr(new TFramedTransport(m_socket));
        m_protocol = TProtocolPtr(new TBinaryProtocol(m_transport));
        m_client = CassandraClientPtr(new CassandraClient(m_protocol));
        m_transport->open();
    }
    
    void Client::SetKeySpace(const string &keyspace) {
        m_client->set_keyspace(keyspace);
    }
    
    static org::apache::cassandra::ConsistencyLevel::type ConvertConsistencyLevel(cassandrawrap::ConsistencyLevel level) {
        return static_cast<org::apache::cassandra::ConsistencyLevel::type>(level);
    }
    
    void Client::Get(org::apache::cassandra::ColumnOrSuperColumn &output,
                     const std::string &key,
                     const org::apache::cassandra::ColumnPath &path,
                     cassandrawrap::ConsistencyLevel level) const {
    
        m_client->get(output,
                      key,
                      path,
                      ConvertConsistencyLevel(level));
    }
    
    string Client::GetVersion() const {
        string version;
        m_client->describe_version(version);
        return version;
    }
    
    void Client::Close(){
        m_transport->close();
    }
    
  • ConsistencyLevel.hpp
  • #ifndef CASSANDRA_CONSISTENCYLEVEL_HPP
    #define CASSANDRA_CONSISTENCYLEVEL_HPP
    
    namespace cassandrawrap {
    
    enum ConsistencyLevel {
        // Values are identical to the thrift definition
        kOne = 1,
        kQuorum = 2,
        kLocalQuorum = 3,
        kEachQuorum = 4,
        kAll = 5,
        kAny = 6
    };
    
    } // namespace
    
    #endif // CASSANDRA_CONSISTENCYLEVEL_HPP
    

SWIGでラップする

いよいよSWIGでラップする部分である。SWIGで何かをラップする場合はまずインタフェース定義というものを作らないといけない。それができたあとはそのインタフェース定義をSWIGに通して、生成されたソースファイルと自分が書いたソースとをコンパイルして完成、ということになる。

インタフェース定義の書き方は、簡単に書くと以下の手順を踏むことになる。

  1. %moduleディレクティブでモジュール名を指定する。
  2. %{から%}の間にコンパイルするのに必要なヘッダを追加する。
  3. それ以外の部分にはSWIGが用意している汎用のインタフェース定義や、自分がラップしたいクラスやら関数の定義が記述されたヘッダを追加する。

ただSWIGは出力が結構カスタマイズできるので、自分好みになるよう色々試行錯誤することができる。今回自分が試したのは、C++で発生した例外をどうやってGoまで上げるかという点。Goにはpanic/recoverといった例外ハンドルの仕組みがあるので、C++で発生した例外でもC++側でcatchせずにGoまで上げた方がGoと親和性が高くなる。

で、色々試した結果が下のcassandra.iにある"%exception"ディレクティブである。このディレクティブを使うと、特定の関数(あるいは関数全部)がどう例外をハンドルするかを指定することができる。これをうまく使うことによって、各exceptionクラスが持っているwhatやらwhyなど、例外が起きた理由をGoのレイヤーまで伝えられるようになる。%exception指定しないと、生成された例外キャッチ部分はどのexceptionクラスが飛んできたかしかGoまで上げてくれず、理由まで見れないのであんまりイケてない。

ちなみに、%exception指定せずに例外キャッチを自動生成させるには、ヘッダと関数の定義に"void SetKeySpace(const std::string &) throw(const apache::thrift::TException);"といった感じで明示的に投げるexceptionを指定しないといけない。

  • cassandra.i(インタフェース定義)
  • %module cassandra
    %{
    // given headers and generated headers
    #include <protocol/TBinaryProtocol.h>
    #include <transport/TSocket.h>
    #include <transport/TTransportUtils.h>
    #include "Cassandra.h"
    
    // my headers
    #include "Client.hpp"
    #include "CassandraAPI.hpp"
    #include "SwigUtility.hpp"
    %}
    
    // added for std::string
    %include stl.i
    
    // added to use primitive types in Go
    %include stdint.i
    
     // added for exception handling
    %include "exception.i"
    
    // thrift related headers
    %include <Thrift.h> // for exception types
    %include <TApplicationException.h> // for exception types
    %include "gen-cpp/cassandra_types.h"
    
    // The following are custom exception handlers for my API
    
    // Basic exception handler
    %exception {
        try {
            $action
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    %exception cassandrawrap::Client::SetKeySpace {
        try {
            $action
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    %exception cassandrawrap::Client::Get {
        try {
            $action
        } catch(const org::apache::cassandra::NotFoundException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::NotFoundException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    // my headers
    %include "ConsistencyLevel.hpp"
    %include "CassandraAPI.hpp"
    %include "Client.hpp"
    
  • SwigUtility.hpp
  • #ifndef CASSANDRA_SWIGUTILITY_HPP
    #define CASSANDRA_SWIGUTILITY_HPP
    
    #include <string>
    
    // utility function for exception handling
    static inline std::string createExceptionString(std::string type, const char* msg) {
        std::string what(msg);
        return type + what;
    }
    
    static inline std::string createExceptionString(std::string type, std::string msg) {
        return type + msg;
    }
    
    #endif // CASSANDRA_SWIGUTILITY_HPP
    
  • cassandra.iで%exception指定した場合の自動生成コード
  • try {
          (arg1)->SetKeySpace((std::string const &)*arg2);
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
          std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
          // any thrift related exception
          std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
          // any other exception
          std::string msg = createExceptionString("std::exception: ", exp.what());
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    
  • cassandra.iで%exception指定しなかった場合の自動生成コード
  • try {
        (arg1)->SetKeySpace((std::string const &)*arg2);
      }
      catch(apache::thrift::TException const &_e) {
        (void)_e;
        _swig_gopanic("C++ apache::thrift::TException const exception thrown");
    
      }
    

(続く)

2011年2月19日土曜日

Go + SWIG + Cassandra + Thrift (3)

前回まででCassandraにサンプルデータが入ったので、今回はその値を取り出すC++のサンプルを作成する。

Cassandraとインタフェースするためのクラスを生成

前回挿入したデータをC++上から取得するにはThriftを使用する必要がある。そこで、まずはThriftを使ってCassandraとインタフェースするためのクラスを生成する。

$ thrift --gen cpp ~/lib/cassandra/interface/cassandra.thrift
$ ls gen-cpp/
cassandra_constants.cpp  Cassandra.cpp  Cassandra_server.skeleton.cpp  cassandra_types.h
cassandra_constants.h    Cassandra.h    cassandra_types.cpp

上記のコマンドで生成されたクラスと、Thriftのライブラリを組み合わせることによってCassandraと通信できるようになる。

Cassandraからデータを取り出すサンプル

#include <string>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>

#include <boost/shared_ptr.hpp>
#include <protocol/TBinaryProtocol.h>
#include <transport/TSocket.h>
#include <transport/TTransportUtils.h>

// auto-generated thrift interface code using cassandra.thrift in cassandra repository
#include "Cassandra.h"

using boost::shared_ptr;
using apache::thrift::protocol::TProtocol;
using apache::thrift::protocol::TBinaryProtocol;
using apache::thrift::transport::TSocket;
using apache::thrift::transport::TTransport;
using apache::thrift::transport::TFramedTransport;
using org::apache::cassandra::CassandraClient;
using org::apache::cassandra::ColumnOrSuperColumn;
using org::apache::cassandra::Column;
using org::apache::cassandra::ColumnPath;
using org::apache::cassandra::ConsistencyLevel;
using org::apache::cassandra::KsDef;
using org::apache::cassandra::InvalidRequestException;
using org::apache::cassandra::NotFoundException;
using apache::thrift::TException;
using std::string;

static void describeServer(shared_ptr<CassandraClient> client, string keyspace) {
    string ver;
    client->describe_version(ver);

    string name;
    client->describe_cluster_name(name);

    KsDef keyspaceDescription;
    client->describe_keyspace(keyspaceDescription, keyspace);

    printf("Server:\n  version: %s\n  name: %s\n", ver.c_str(), name.c_str());
    printf("  keyspace: %s\n  strategy class: %s\n",
           keyspaceDescription.name.c_str(),
           keyspaceDescription.strategy_class.c_str());
}

static void printColumn(const Column &col) {
    printf("Column:\n  name: %s\n  value: %s\n  timestamp: %lld\n  ttl: %d\n",
           col.name.c_str(),
           col.value.c_str(),
           col.timestamp,
           col.ttl);
}

int main(int argc, char **argv) {
    const string host = "localhost";
    int port = 9160;
    shared_ptr<TTransport> socket(new TSocket("localhost", port));
    shared_ptr<TTransport> transport(new TFramedTransport(socket));
    shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));
    shared_ptr<CassandraClient> client(new CassandraClient(protocol));
    transport->open();

    const string keyspace = "sample_keyspace";

    describeServer(client, keyspace);
    
    client->set_keyspace(keyspace);

    string columnFamily = "SampleColumnFamily";
    string columnKey = "SampleColumn";
    string columnName = "SampleName";
    ColumnPath path;
    path.__isset.column = true;
    path.column_family = columnFamily;
    path.column = columnName;

    ColumnOrSuperColumn output;
    try {
        client->get(output,
                    columnKey,
                    path,
                    ConsistencyLevel::ONE);
    } catch (InvalidRequestException &re) {
        printf("ERROR: InvalidRequest. %s\n",re.why.c_str());
    } catch (NotFoundException &e) {
        printf("ERROR: NotFound. %s\n",e.what());
    }

    printColumn(output.column);

    transport->close();
}

ビルド方法

最初にThriftで生成したソース、及び上のサンプルを記述したsample.cppを合わせてビルドする。

$ g++ -Wall sample.cpp gen-cpp/cassandra_types.cpp gen-cpp/cassandra_constants.cpp gen-cpp/Cassandra.cpp -I/usr/local/include/thrift -I./gen-cpp -lthrift

実行結果

$ ./a.out
Server:
  version: 19.4.0
  name: Test Cluster
  keyspace: sample_keyspace
  strategy class: org.apache.cassandra.locator.SimpleStrategy
Column:
  name: SampleName
  value: SampleValue
  timestamp: 1297591990231000
  ttl: 0

(続く)

2011年2月13日日曜日

Go + SWIG + Cassandra + Thrift (2)

前回まででインストールが完了したので、今度はThriftを使用してCassandraとデータのやりとりを行うところをやっていく。

Cassandraにサンプルデータをセットする

Cassandraとデータのやりとりをするまえに、まずはCassandra側にサンプルのデータを配置する必要がある。ということで、サンプルのデータをCassandraに追加していく。

  1. Cassandraを起動する
  2. $ cassandra -f
    
  3. ターミナルをもう一つ開いて(Cassandraが起動している方はそのまま)、CassandraのCUI版インタフェースを起動する。CUI版が起動すると独自のコマンドラインインタフェースへ移行する
  4. $ cassandra-cli  --host localhost --port 9160
    Connected to: "Test Cluster" on localhost/9160
    Welcome to cassandra CLI.
    
    Type 'help;' or '?' for help. Type 'quit;' or 'exit;' to quit.
    [default@unknown]
    
  5. Keyspaceを生成する。KeyspaceはCassandraが持つ最も大きなくくりで、アプリケーション単位では別々のKeyspaceを使用することが多い。Cassandra自体のより詳しい説明についてはここのwiki等を参照。
  6. [default@unknown] create keyspace sample_keyspace;
    d8bdc2a3-3754-11e0-bdc2-e700f669bcfc
    [default@unknown]
    
  7. 使用するKeyspaceを宣言する。
  8. [default@unknown] use sample_keyspace;
    Authenticated to keyspace: sample_keyspace
    
  9. 適当なColumn Familyを生成する。Column FamilyはKeyspaceの次に大きなくくりで、一つのアプリケーション内で複数のColumn Familyを持つことになる。
  10. [default@sample_keyspace] create column family SampleColumnFamily with comparator=UTF8Type;
    8ff0baa5-3759-11e0-bdc2-e700f669bcfc
    
  11. 適当なColumnを追加する。ColumnはCassandraのなかで最も小さいくくりで、キーと値と時間がセットになっている。
  12. [default@sample_keyspace] set SampleColumnFamily[SampleColumn][SampleName] = SampleValue;
    Value inserted.
    
  13. 値がちゃんと追加されたことを確認する。なお、getするときには明示的に型を指定しないと正しく値が表示されない。
  14. [default@sample_keyspace] get SampleColumnFamily[SampleColumn][SampleName];
    => (column=SampleName, value=53616d706c6556616c7565, timestamp=1297591990231000)
    [default@sample_keyspace] get SampleColumnFamily[SampleColumn][SampleName] as UTF8Type;
    => (column=SampleName, value=SampleValue, timestamp=1297591990231000)

(続く)

2011年2月12日土曜日

Go + SWIG + Cassandra + Thrift

2011年2月現在、Goにはデータベースとやりとりするためのバインディングが無い。Goパッケージのダッシュボードには色んな人が作ったパッケージがあるけど、どれも有志が作ったもので例えばMySQLが公式にGoをサポートしてくれるわけではない。なので、Goを使ってDBとやりとりして何かしようと思うとちょっと面倒だったりする。

そんな状況で、公式なGoバインディングが登場するまでのつなぎとしてGo + SWIGという選択肢がある。今回はそのGo + SWIGからCassandraを呼ぶことで、GoからのDB利用を試すことにする。

SWIGとは

SWIGはPythonとかの言語からC/C++のライブラリが呼べるようにするツールだと思ってもらえればいい。SWIG v2.02から(正確にはr12398から)はGoからC/C++のライブラリを呼べるようになったので、C/C++でデータベースとやりとりするラッパーを作って、それをGoから呼ぶようにすれば半分公式サポートされてるような状態にできる。これだったら、どこの誰がメンテしてるかわからない野良パッケージに頼るよりは安心して使える。

Cassandraとは

Cassandraは最近はやりのNoSQL系データベース、らしい。良く知らないけどmemcachedとかのkey/value系データベースみたいな。Read/Writeが高速にできて、APIもわりとシンプルなのでこれを今回使うことにする。

Thriftとは

Cassandraはちょっと凝った?ことをやっていて、データベース本体とインタフェースするためのライブラリが別に存在する。それがThriftというライブラリで、CassandraはいまのところThrift経由でアクセスするのが正攻法らしい。多分これはCassandraのエンジン部分とインタフェース部分を分離したかったからだろうけど、とにかくユーザーはCassandraを使おうと思ったらThriftを経由することになる。

Thrift自体は単体でも使用できて、サーバーサイドとクライアントサイドが通信するための色々なことを肩代わりしてくれる便利ライブラリらしい(適当)。

準備

では前置きがすんだところで実際の準備へ入る。

SWIGのインストール

  1. svnのリポジトリから最新を持ってくる。公式にリリースされてるSWIG v2.01は最新のGoとの連携が壊れてるので、最新のGo(release.2011-02-01)と連携するならsvnからの最新、最低でもr12398以上が必要。
  2. $ svn co https://swig.svn.sourceforge.net/svnroot/swig/trunk swig
    
  3. SWIGをビルド、インストールする。実行も確認する。
  4. $ cd swig
    $ ./autogen.sh
    $ ./configure --with-go=/home/masato/lib/google-go/bin/8g
    $ make && sudo make install
    $ swig
    Must specify an input file. Use -help for available options.
    

Cassandraのインストール

  1. Cassandra公式ウェブサイトへ行って最新のバイナリ(現在0.7.0)をダウンロードして、自分のhome以下に展開。なんならシンボリックリンクでも作ってバージョンを隠蔽する。
  2. $ cd ~/lib
    $ tar xzf apache-cassandra-0.7.0-bin.tar.gz
    $ ln -s apache-cassandra-0.7.0/ cassandra
    
  3. .bashrcに記述するなりしてcassandraのバイナリヘパスを通す。
  4. export PATH=$PATH:~/lib/cassandra/bin
    
  5. cassandraを実行するのに必要なディレクトリを作成して、パーミッションを正しく設定する
  6. $ sudo mkdir -p /var/log/cassandra
    $ sudo mkdir -p /var/lib/cassandra
    $ sudo chown -R `whoami` /var/log/cassandra/
    $ sudo chown -R `whoami` /var/lib/cassandra/
    
  7. cassandraを起動する。特に例外が発生することなくそのまま実行されてれば成功。
  8. $ cassandra -f
     INFO 10:18:16,041 Heap size: 2087124992/2102198272
     INFO 10:18:16,045 JNA not found. Native methods will be disabled.
    ...
    

Thriftのインストール

  1. Thrift公式ウェブサイトへ行って最新のソース(現在0.6.0)をダウンロード、解凍する。
  2. $ tar xzvf thrift-0.6.0.tar.gz
    
  3. ビルド、インストールする。単純にビルドしようとすると色々要求されたので、今回はいくつかのオプションをオフにしてビルドした。人によっては他にもpythonやらを無効にするのもあり。
  4. $ ./configure --without-java --without-java_extension --without-php --without-php_extension --with-cpp
    $ make && sudo make install
    
  5. 実行を確認。
  6. $ thrift
    Usage: thrift [options] file
    ...
    

(続く)