2011年12月10日土曜日

Using Kensington Expert Mouse with Ubuntu 11.10

自宅ではKensington Expert Mouseを使ってるんだけど、普段左手でマウスを使うようにしてるのでボタンのマップをいじる必要がある。それで結構前に色々調べた結果、以下のコマンドをやれば理想的なボタン配置(左上:右クリック、右上:右クリック、左下:ミドルクリック、右下:左クリック、反時計回りスクロール:下降、時計回りスクロール:上昇)になることまではわかった。

/usr/bin/xinput set-button-map "Kensington      Kensington Expert Mouse" 2 3 1 5 4 3 3 3 3 3 3 3

あとはこれを~/.xinitrcに記述して起動時に適切なボタンにマップされるところまでは良かった。

USB抜きさしすると初期化される

ただこのやり方には一つ穴があって、マウスをUSBから抜きさしすると適用したマップが初期化されてしまう、というもの。当時のUbuntuはHALを無くすとかなんとかでハードウェアまわりのソフトウェア環境がゴタゴタしてて、色々調べたけどいまいちこの問題を解消する方法がわからなかった。で、昨日久しぶりに調べてみたらあっさり解決したのでここにメモっておく。

  1. lsusbコマンドを実行して、Kensingtonマウスの項目にあたる部分をひかえておく。
    $ lsusb
    ...
    Bus 004 Device 008: ID 047d:1020 Kensington Expert Mouse Trackball
    ...
    
  2. /usr/share/X11/xorg.conf.d/90-kensington-expert.confというファイルを作り、中身を以下のように書く。 IdentifierとMatchUSBIDはlsusbで出力されたものを使う。
    Section "InputClass"
            Identifier      "Kensington Expert Mouse Trackball"
            MatchUSBID      "047d:1020"
            Option "ButtonMapping" "2 3 1 5 4 3 3 3 3 3 3 3"
    EndSection
    
  3. 一旦ログアウトして、再度ログインする。

以上おしまい。もっと早く調べておけば良かった。

2011年11月12日土曜日

Setting up Google App Engine Environment (Go) for Ubuntu 11.10

Ubuntu 11.10でGoogle App Engine (Go)の環境をセットアップするのに手間取ったのでメモ。

Google App Engine SDKの取得

いまの最新版SDK1.6.0を落としてセットアップ。パスも適当に通す。

$ cd ~/lib
$ wget http://googleappengine.googlecode.com/files/go_appengine_sdk_linux_386-1.6.0.zip
$ unzip go_appengine_sdk_linux_386-1.6.0.zip
$ PATH=$PATH:~/lib/google_appengine

Python 2.5のセットアップ

Google App Engineの開発用ローカルサーバー(dev_appserver.py)を正常に動作させるにはまだPython 2.5が必要なため、インストールする。ただUbuntu 11.10はもうPython 2.5を切りすててしまっているため、外部のリポジトリから入手する必要がある。

$ sudo add-apt-repository ppa:fkrull/deadsnakes
$ sudo apt-get update
$ sudo apt-get install python2.5 python2.5-dev

PILのセットアップ

PILがインストールされていないとdev_appserver.pyの起動時に"Could not initialize images API; you are likely missing the Python "PIL" module."というエラーが出るため、PILもインストールする。

$ wget http://effbot.org/downloads/Imaging-1.1.7.tar.gz 
$ tar xzf Imaging-1.1.7.tar.gz 
$ cd Imaging-1.1.7/
$ sudo python2.5 setup.py install

これであとは下のような感じでdev_appserver.pyを起動すればいける。

$ python2.5 ~/lib/google_appengine/dev_appserver.py server/ 

Python 2.7でやろうとすると

そもそもなんでこんなことを今さらやったかというと、Python 2.7でdev_appserver.pyを実行すると、ときたま落ちるから。試しにChannel APIを使おうとしたら↓のエラーで死ぬようになってしまったため、やはり正規のPython 2.5を入れる必要がでた、ということ。

INFO     2011-11-12 14:29:41,093 dev_appserver.py:2753] "POST /_ah/channel/connected/ HTTP/1.1" 404 -
----------------------------------------
Exception happened during processing of request from ('0.1.0.10', 80)
Traceback (most recent call last):
  File "/usr/lib/python2.7/SocketServer.py", line 284, in _handle_request_noblock
    self.process_request(request, client_address)
  File "/usr/lib/python2.7/SocketServer.py", line 311, in process_request
    self.shutdown_request(request)
  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)
AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'
----------------------------------------
ERROR    2011-11-12 14:29:41,128 dev_appserver_main.py:664] Error encountered:
Traceback (most recent call last):

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver_main.py", line 657, in main
    http_server.serve_forever()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3527, in serve_forever
    self.handle_request()

  File "/home/masato/lib/google_appengine/google/appengine/tools/dev_appserver.py", line 3490, in handle_request
    self._handle_request_noblock()

  File "/usr/lib/python2.7/SocketServer.py", line 287, in _handle_request_noblock
    self.shutdown_request(request)

  File "/usr/lib/python2.7/SocketServer.py", line 459, in shutdown_request
    request.shutdown(socket.SHUT_WR)

AttributeError: 'ChannelPresenceConnection' object has no attribute 'shutdown'

Now terminating.

2011年9月17日土曜日

Goで○○する方法 part 1

GDD 2011のチャレンジ問題をGoで解こうとコードを書いたのはいいけれど、結構前から存在してたらしいGCのバグに気付かずなんでメモリ切れになっちゃうんだろうなーと激しく時間のムダをしてしまった。で、結局C++に書きなおした。Goはデバッガもいまいちまだ整備されてないし、やっぱここぞというときは信頼できる枯れた言語と実行環境でやるのが重要っすね。

さてそんな感じにGDD 2011をやってて思ったけど、Goはやっぱり致命的にサンプルコードが足りない気がした。単純にファイルを開いてテキストを一行ずつ読む、という基本的なことすらやり方がよくわからなかったりする。言語の名前が検索しづらい、というのもそれに拍車をかけてる。

ということで、そんな基本的なコードをいくつか紹介していくことにする。

コマンドライン引数をパースして値を取得する方法

プログラム書いてるときに良く書くコードとして、コマンドライン引数をパースしてそれを取り込む、というものがあげられる。更にはhオプション付きで実行されたらヘルプドキュメントを出力する、といったことも良くやる。Goにはその辺を簡単にしてくれるflagパッケージというものが存在している。

以下はGDD 2011のチャレンジ問題を解く(はずだった)プログラムの一部で、コマンドライン引数をパースしている。

func main() {
    var inputFile *string = flag.String("in", "", "Path to the input file")
    var outputFile *string = flag.String("out", "", "Path to the output file")
    flag.Parse()
    logger.Println("Input file path:", *inputFile)
    logger.Println("Output file path:", *outputFile)
    ...
}

このコードのおかげで、以下のように引数が指定できるようになる。

$ ./SlidePuzzle -in puzzle.txt -out output.txt
Input file path: puzzle.txt
Output file path: output.txt

もし変なオプションを指定したとしたら、ちゃんとヘルプまで表示してくれる。

$ ./SlidePuzzle -aardvark
flag provided but not defined: -aardvark
Usage of ./SlidePuzzle:
  -in="": Path to the input file
  -out="": Path to the output file
$

そんな便利なパッケージです。みなさん使いましょう。

テキストファイルを開いて一行づつ読む方法

前項の方法でファイルパスを得たら、今度はそのファイルを開いて読んでいきたいと思うのが普通の人。ということでそのやり方となるのが下の方法。

    inFile,err := os.Open(*inputFile)
    if err != nil {
        logger.Println(err)
        return
    }

    defer func() {
        inFile.Close()
    }()

    reader := bufio.NewReader(inFile)

    // Get the maxinum number of moves allowed
    numberOfHands, err := reader.ReadString('\n')
    if err != nil {
        logger.Println(err)
        return
    }

    var maxLeft, maxRight, maxUp, maxDown int
    fmt.Sscanf(numberOfHands, "%d %d %d %d\n", &maxLeft, &maxRight, &maxUp, &maxDown)
    logger.Printf("Left: %d Right: %d Up: %d Down: %d\n", maxLeft, maxRight, maxUp, maxDown)

入力となるのが下のテキスト。

72187 81749 72303 81778

手順としては以下のようにやっている。

  1. ファイルを開く
  2. ファイルがちゃんと閉じられるようdeferで指定する
  3. 開いたファイルからbufio.Readerを生成する
  4. bufio.Readerで一行読む
  5. 読みこんだ一行をfmt.Sscanfでパースする

上のやり方はわりと普通だけど、一点注意が必要なのはbufio.Readerを生成するところ。ここはGoのインタフェースの仕組みがわかってないと「?」となる。

インタフェースを活用したパッケージ

Goはインタフェースさえ実装されてれば何でもカモン、というスタンスの言語だけど、それはもちろんライブラリにも適用されている。たとえばbufio.NewReader()のインタフェースは下の通り。

func NewReader(rd io.Reader) *Reader

で、io.Readerのインタフェースは以下の通り。

type Reader interface {
    Read(p []byte) (n int, err os.Error)
}

従来のオブジェクト指向言語に慣れてると「io.Readerというインタフェースを実装したインスタンスをbufio.NewReader()に渡せばいい」と考えてしまって、それはある意味正しいんだけど、Goだと少し注意が必要。なぜなら、Goではどの型がどのインタフェースを実装しているかが明示されていないから。

実際、os.Openから返ってくるFileという型のドキュメントを見ると、そこにはio.Readerというインタフェースの名前は一切出てこない。書いてあるのはこのFileという型が実装している各種の関数だけ。でもよくよく見ると、実はFileがio.Readerのインタフェースを満たしていることがわかる。下はFile.Read()のインタフェース。

func (file *File) Read(b []byte) (n int, err Error)

これ、io.Readerが定義しているインタフェースそのまま。なので、Fileはbufio.NewReaderにそのまま渡して問題無いということ。このように、いまのドキュメントだとどの型がどのインタフェースを実装しているのかがわかりにくいため、パッケージ同士がどう連携するかがつかめなくてちょっとしたことをするにも意外と苦労することになる。

まとめ

  • flagパッケージでコマンドライン引数をパースすると楽。
  • 各種パッケージ間の連携はインタフェースによってのみ定義されており、ドキュメント上のどこかに明示されているわけではない。なので、パッケージ間で連携させるような処理を書く場合はどの型がどのインタフェースを実装しているか注意してドキュメントを読む必要がある。

2011年8月7日日曜日

Creating an original REST API using Google App Engine and Go (7)

前回でDELETEのAPIができたので、今回は最後になるPUTを実装する。

コメント更新用のAPI

従来の作りを踏襲して以下の形にする。

http://localhost:8080/1/remark/put/[コメントのID]

サーバーサイド

実装してみたところ、PUTが一番めんどうなことになっていた。DELETEのときと一緒で、PUT用のAPIとかが存在しないので、手動で色々やる必要がある。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
    http.HandleFunc("/1/remark/put/", errorHandler(putRemark))
}
...

// Handle a put request of remarks
func putRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case PUT:
        c.Infof("Putting Remark\n")
        c.Infof("Request: %s\n", r)
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)

        // check if we got the updated remark
        buf := bytes.NewBuffer(nil);
        _, err := buf.ReadFrom(r.Body)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        bodyStr := buf.String()
        c.Infof("body: %s\n", bodyStr)
        values, err := http.ParseQuery(bodyStr)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        remarkString, present := values[REMARK_FIELD]
        if !present {
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
            return
        }

        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // update remark of given id
        itr := query.Run(c)
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Updating id:%d remark:%s", key.IntID(), remark.Content)

            store := Remark {
                Content : remarkString[0],
                Time : datastore.SecondsToTime(key.IntID()), 
            }

            _, err = datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

POSTのときと違ってParseForm()が使えないので、自分でBodyを読んでhttp.ParseQuery()を呼んで、という風な手順を踏まないといけくなっている。ただリクエストをパースする部分さえできてしまえば、あとは他のAPIとだいたい同じ。

クライアントサイド

クライアントサイドは今までAPIごとに用意してきたクライアントを一つに統合して、引数で実行を制御するように変更した。

package main

import (
    "http"
    "fmt"
    "os"
    "bytes"
    "flag"
    "strconv"
)

// print the body of the given response
func printResponse(r *http.Response) {
    fmt.Println(r.Status)
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}


func main() {
    id := flag.Int64("id", 0, "Set the id of the remark")
    method := flag.Int("method", 0, "Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3")
    remark := flag.String("remark", "", "Set the remark string")
    flag.Parse()

    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    switch (*method) {
    case 0: // GET
        getUrl := remarkUrlRoot + "get"
        r, err := client.Get(getUrl)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 1: // POST
        postUrl := remarkUrlRoot + "post/"
        data := http.Values { 
            "remark": {*remark,},
        }
        r, err := client.PostForm(postUrl, data)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 2: // DELETE
        deleteUrl := remarkUrlRoot + "delete/" + strconv.Itoa64(*id)
        fmt.Println(deleteUrl)
        request, err := http.NewRequest("DELETE", deleteUrl, nil)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    case 3: // PUT
        putUrl := remarkUrlRoot + "put/" + strconv.Itoa64(*id)
        putBuf := bytes.NewBuffer([]byte("remark="+*remark))
        request, err := http.NewRequest("PUT", putUrl, putBuf)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        request.Header.Add("Content-Length", string(putBuf.Len()))
        r, err := client.Do(request)
        if err != nil {
            fmt.Printf("Error: %s",err)
            return
        }
        printResponse(r)
    default:
        fmt.Println("Unknown method")
        return 
    }
}

PUTのときはPOSTのマネをしてContent-Typeにapplication/x-www-form-urlencodedを設定して送っている。この辺はもっと別のやり方でもいいと思う。

クライアント実行例

$  ./client -h
flag provided but not defined: -h
Usage of ./client:
  -id=0: Set the id of the remark
  -method=0: Set the method to use. GET: 0, POST:1, DELETE:2, PUT:3
  -remark="": Set the remark string

$ ./client
200 OK
id=1312687737000000&remark=asfasdfasdfas
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

Finished reading
$ ./client -method=2 -id=1312687737000000
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=aardvark

$ ./client -method=3 -id=1312687246000000 -remark=thunderbolt
200 OK
Finished reading

$ ./client
200 OK
id=1312687714000000&remark=aardvark
id=1312687246000000&remark=thunderbolt

Finished reading

サーバーサイド出力(PUTしたとき)

2011/08/07 06:28:45 INFO: Putting Remark
2011/08/07 06:28:45 INFO: Requested URL: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: Requested URL path: /1/remark/put/1312687246000000
2011/08/07 06:28:45 INFO: body: remark=thunderbolt
2011/08/07 06:28:45 INFO: Updating id:1312687246 remark:aardvark
INFO     2011-08-07 06:28:45,116 dev_appserver.py:4248] "PUT /1/remark/put/1312687246000000 HTTP/1.1" 200 -

まとめ

ということで数回に渡ってGo + Google App EngineでRESTなAPIの基礎的な部分を試してきた。一番厄介だったのはhttpパッケージまわりで、サンプルコードもあまり無いため探りつつ実行する、という感じだった。逆にGoogle App Engine側はそれほど探ることなく、素直に書いて素直に実行できた気がする。

連載中にGoogle App Engine側もアップデートをして、Go版も色々と進化してきたのでまたおいおい別の記事も書いていきたい。

2011年8月6日土曜日

Tuning Ubuntu's Unity Desktop

普段Google App Engine関連の開発にはUbuntu 11.04を使ってて、評判はともかくとしてUnityはわりと気にいっている。まぁGnome 3もいつかは試したいけど。

Alt+Tabの切り替えが遅い

ただ前から気になってる点があって、なにかというとAlt+Tabでウィンドウを切り替えたときの微妙なもたつき。正直切り替えはもっとサクサク動いて欲しい。しばらく我慢して使ってたけど耐えられなくなってきたので、設定で改善できないか探したらわりとあっさり見つかった。

Compiz Config Settings Manager

まず必要なのはCompiz Config Settings Manager。これたしか標準では入ってないんだけど、Unityまわりのチューニングやらキーバインドを変えようと思ったら必要。自分はこれでウィンドウのタイル配置キーバインド(Ctrl+Alt+5とか)も変えている。テンキーとか無いからー。

Compiz Config Settings Managerをインストールするには以下。

sudo apt-get install compizconfig-settings-manager

表示速度の改善

Compiz Config Settings Managerがインストールされたら、ccsmコマンドで起動して、以下の手順で速度を調整する。

  1. ウィンドウ・マネジメントの項目にあるStatic Application Swicherをクリックする。
  2. Behaviourタブに移動する。
  3. Popup Window Delayを0にする。

以上。これでAlt+Tabを押したらウィンドウ候補が即表示されるようになる。ちなみに、これは値を変えた時点で即反映されるはず。

ワークスペース切り替えもついでに

自分はついでにワークスペース間の切り替えも高速化してて、その方法は以下。

  1. デスクトップの項目にあるDesktop Wallをクリックする。
  2. Viewport Switchingのタブに移動する。
  3. Wall Sliding Durationを0にする。

これでワークスペースをCtrl+Alt+右矢印とかで切りかえたときのスライドアニメーションが無くなって、高速化される。

2011年8月2日火曜日

Creating an original REST API using Google App Engine and Go (6)

前回から引きつづいて、今度はDELETE用のAPIを実装していく。

コメント削除用のAPI

REST的には削除対象はURLで表現する形になるので、今回もそれにならったAPIとする。

http://localhost:8080/1/remark/delete/[コメントのID]

サーバーサイド

API的にはgetに似ているけど、コード自体もかなりgetと似た形になる。

...
func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
    http.HandleFunc("/1/remark/delete/", errorHandler(deleteRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if (split[i] == "get" || split[i] == "delete") &&
            (i+1 < length) { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}
...
// Handle a delete request of remarks
func deleteRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case DELETE:
        c.Infof("Deleteing Remark\n")
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        if !foundID {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        } else { // foun ID within URL
            query := datastore.NewQuery(REMARK_KIND).Filter("Time=", id)

            // check if any results exist
            count, err := query.Count(c)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            
            if count == 0 {
                msg := "Specified remark does not exist\n"
                c.Infof(msg)
                http.Error(w, msg, http.StatusNotFound)
                return
            }

            // remove remark with given ID
            itr := query.Run(c)
            for {
                var remark Remark
                key, err := itr.Next(&remark)
                if err == datastore.Done {
                    break
                } else if err != nil {
                    http.Error(w, err.String(), http.StatusInternalServerError)
                    return
                }
                c.Infof("Removing id:%d remark:%s", key.IntID(), remark.Content)
                datastore.Delete(c, key)
            }
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

getと違うのはIDが指定されなかったらエラーを返す点と、クエリーの結果見つかったkeyに対してdatastore.Deleteを呼んで、keyを消してる点くらい。

クライアントサイド

クライアントサイドもやっぱりgetと限りなく似ていて、DELETEは若干マイナーなのか、GETみたいな便利なAPIが無いからhttp.Requestを自分で作ってからhttp.Client.Doを呼ぶ一手間が増えたくらい。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    deleteUrl := remarkUrlRoot + "delete/1312292876000000"
    request, err := http.NewRequest("DELETE", deleteUrl, nil)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    
    r, err := client.Do(request)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

INFO     2011-08-02 13:55:43,693 __init__.py:347] building _go_app
INFO     2011-08-02 13:55:44,656 __init__.py:333] running _go_app
2011/08/02 13:55:44 INFO: Deleteing Remark
2011/08/02 13:55:44 INFO: Requested URL: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Requested URL path: /1/remark/delete/1312292876000000
2011/08/02 13:55:44 INFO: Removing id:1312292876 remark:aardvark
INFO     2011-08-02 13:55:44,771 dev_appserver.py:4248] "DELETE /1/remark/delete/1312292876000000 HTTP/1.1" 200 -

クライアントサイドの出力

$ ./client 
200 OK
Finished reading

次回

次回はPUT。それでこの連載は一旦終わりかなー。

(続く)

2011年7月23日土曜日

Creating an original REST API using Google App Engine and Go (5.1)

2011/8/2追記: コード修正

REST APIを作るための記事を名乗っていながら、前回書いた記事では全然RESTっぽいことやってなかったことに気付いた。ということでAPI部分から変えてしきりなおし!

コメント取得用のAPI

RESTは(GETに関して言えば)極力URLで表現しようよ、というスタンスなので、前回のようにパラメータで欲しいコメントを取得するのは実はあまりREST的でない。なので、APIもよりREST的な形に変更する。

http://localhost:8080/1/remark/get/[コメントのID]

前回と違って、get以降に取得したいコメントのIDをそのままURLで指定するようにした。

サーバーサイド

APIの変更にあわせてサーバーサイドのコードも少し変える。前回からの変更点は以下の通り。

  • ".../get/"以降に直接コメントのURLが指定されてたら指定されたコメントだけを返す。
  • ".../get/"までしか指定されてなかったらコメントを全て返す。

当然、存在しないコメントのIDを指定された場合の適切なエラー処理も必要。ということで以下がそのコード。

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post/", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get/", errorHandler(getRemark))
}
...
func getRemarkID(path string) (int64, bool) {
    split := strings.Split(path, "/", -1)
    length := len(split)
    for i := 0; i<len(split); i++ {
        if split[i] == "get" &&
            i+1 < length { // next word may not exist
            id, err := strconv.Atoi64(split[i+1])
            return id, (err == nil)
        }
    }
    return 0,false
}

// Handle a get request of remarks
func getRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case GET:
        c.Infof("Requested URL: %s\n", r.RawURL)
        c.Infof("Requested URL path: %s\n", r.URL.Path)
        // Check if we were requested for a specific remark
        id, foundID := getRemarkID(r.URL.Path)
        
        var query *datastore.Query
        if  foundID {
            // If we were, create a query to get that remark
            c.Infof("Retrieving specified remark id: %d\n", id)
            query = datastore.NewQuery(REMARK_KIND).Filter("Time=", datastore.Time(id))
        } else {
            // If we weren't, create a query to get all the remarks (new remarks come first)
            c.Infof("Retrieving all remarks\n")
            query = datastore.NewQuery(REMARK_KIND).Order("-Time")
        }
        
        // check if any results exist
        count, err := query.Count(c)
        if err != nil {
            http.Error(w, err.String(), http.StatusInternalServerError)
            return
        }
        
        if count == 0 {
            msg := "Specified remark does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusNotFound)
            return
        }
        
        // We have a result.
        // query for it.
        itr := query.Run(c)
        buf := bytes.NewBuffer(nil) // a variable sized buffer
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                return
            }
            c.Infof("Retrieved id:%d remark:%s", key.IntID(), remark.Content)
            fmt.Fprintf(buf, "id=%d&remark=%s\n", remark.Time, remark.Content)
        }

        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        io.Copy(w, buf)
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}
...

前回よりコードが長くなっている。注意するのは、http.HandleFunc("/1/remark/get/", errorHandler(getRemark))のようにハンドラを指定するとき、URLの末尾にはしっかりスラッシュをつけるということ。こうすることで、getから先にまだURLが続いていても同じハンドラ関数が呼ばれるようになる。このスラッシュをちゃんと指定しないと、"/1/remark/get/123151..."のようなURLへリクエストを出したときはgetRemarkハンドラが呼ばれなくなってしまう。

クライアントサイド

クライアント側はコメントのIDをURLに追加するくらいで、それほど変更点は無い。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"

    getUrl := remarkUrlRoot + "get/1311387270000000"
    r, err := client.Get(getUrl)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

サーバーサイドの出力

2011/07/23 12:17:09 INFO: Requested URL: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Requested URL path: /1/remark/get/1311387270000000
2011/07/23 12:17:09 INFO: Retrieving specified remark id: 1311387270000000
2011/07/23 12:17:09 INFO: Retrieved id:1311387270 remark:aardvark
INFO     2011-07-23 12:17:09,169 dev_appserver.py:4248] "GET /1/remark/get/1311387270000000 HTTP/1.1" 200 -

クライアントサイドの出力

200 OK
id=1311387270000000&remark=aardvark

Finished reading

次回

ということで改めてGET用のAPIを作ったところで、次回はDELETEあたりをやることにする。

(続く)

2011年6月27日月曜日

Creating an original REST API using Google App Engine and Go (5)

2011/7/23追記: r58.1に伴うAPIの変更部分を修正

前回まででコメントの格納ができるようになった(っぽい)ので、今度はそれを取得できるようにして実際入ってることを確認する。

コメント取得用のAPI

まずはコメント取得用のAPIを作るところから考える。投稿用のAPIと同じ形を踏襲するので、特に深く考えることなく以下のURLにする。

http://localhost:8080/1/remark/get

あとはここのURLに対してのリクエストをハンドルする処理をサーバーサイドに追加していく。

サーバーサイド

そろそろ長くなってきたので、差分を載せていくことにする。下記がコメントの取得用APIを追加した部分のコード。

...
func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post", errorHandler(postRemark))
    http.HandleFunc("/1/remark/get", errorHandler(getRemark))
}
...

// Handle a get request of remarks
func getRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    switch r.Method {
    case GET:
        c.Infof("Retrieving Remarks\n")

        // get all the remarks (new remarks come first)
        query := datastore.NewQuery(REMARK_KIND).Order("-Time")
        itr := query.Run(c)

        buf := bytes.NewBuffer(nil) // a variable sized buffer
        for {
            var remark Remark
            key, err := itr.Next(&remark)
            if err == datastore.Done {
                break
            } else if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
                break
            }
            c.Infof("Retrieved id:%d remark:%s", key.IntID(), remark.Content)
            fmt.Fprintf(buf, "id=%d&remark=%s\n", remark.Time, remark.Content)
        }
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        io.Copy(w, buf)
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

処理の流れは以下の通り。重要なのは主にクエリをどう生成するか、という部分だろうか。

  1. コメント取得用のクエリを生成する
  2. クエリを実行して返ってきた結果を走査しつつ、レスポンス用にデータを整形する
  3. 適切なヘッダを付加してレスポンスを送り返す

ちょっとわかりづらいのはOrder("-Time")という部分で、ここは取得したデータのうち、Timeというフィールドの値の大小に応じて並びかえろと指定している。TimeというフィールドはRemark構造体に自分で勝手に定義したフィールドだから、別にここに来るフィールド名自体は何でもいい。あと、デフォルトでは昇順で動くので、頭にマイナス記号をつけて降順にしている。こうすることで、時間的には新しいコメントが先にくるよう並びかえられる、というわけ。

あと今は特に引数をあたえることなく決まった動作(全部のコメントを新しい順に取得する)しかしてないけど、getのAPI的にはもっと細かな制御ができるようにしないといけない。でもそれはひとまずあとまわし。

クライアントサイド

引き続きクライアントサイドのコード。こっちはまだ短いので全部載せる。

package main

import (
    "http"
    "fmt"
    "os"
)

func main() {
    client := new(http.Client)
    remarkUrlRoot := "http://localhost:8080/1/remark/"
    getUrl := remarkUrlRoot + "get"
    r, err := client.Get(getUrl)
    if err != nil {
        fmt.Printf("Error: %s",err)
        return
    }
    fmt.Println(r.Status)
    
    bufSize := 256
    buf := make([]byte, bufSize)
    for {
        read, err := r.Body.Read(buf)
        if read == 0 && err == os.EOF {
            fmt.Println("Finished reading")
            break
        } else if err != nil {
            fmt.Printf("Error during read: %s",err)
            break
        }
        // convert the buffer to a string and print it
        fmt.Println(string(buf[0:read]))
    }
    r.Body.Close()
}

指定されたURLにGetを発行して、返ってきたResponseのBodyを読みつつ表示する、という処理をやっている。決まりきった動作なので特に説明するところは無さそう。Goではこうやるんですよー的な見本かな。

出力結果

サーバーサイドとクライアントサイドの出力結果をそれぞれのせておく。ちゃんとクエリ通りの順番で取得できていることが確認できる。

サーバーサイドの出力結果

INFO     2011-06-26 15:19:20,629 __init__.py:324] building _go_app
INFO     2011-06-26 15:19:21,554 __init__.py:316] running _go_app
2011/06/26 15:19:21 INFO: Retrieving Remarks
2011/06/26 15:19:21 INFO: Retrieved id:1309056182 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1309056155 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1308993994 remark:aardvark
2011/06/26 15:19:21 INFO: Retrieved id:1308993976 remark:aardvark
INFO     2011-06-26 15:19:21,671 dev_appserver.py:4217] "GET /1/remark/get HTTP/1.1" 200 -

クライアントサイドの出力結果

200 OK
id=1309056182000000&remark=aardvark
id=1309056155000000&remark=aardvark
id=1308993994000000&remark=aardvark
id=1308993976000000&remark=aardvark

Finished reading

次回

PostしてGetするところまではできたから、残るは(REST的には)DeleteとPut。ということで、次はたぶん投稿したコメントを削除するDeleteのAPIをやるかな。

(続く)

2011年6月26日日曜日

Creating an original REST API using Google App Engine and Go (4)

前回まででコメントを送信する部分はできたので、今回はそのコメントを格納する部分をやる。

Datastore

Google App Engineは自前でDatastoreというデータベースを持っていて、何らかのデータを格納しようと思ったらそこを使うことになる。詳しくは公式のドキュメントを読んでもらうとして、ざっくりと特徴をまとめると以下のようになる。

  • 超スケールする
  • 色んな場所に自動で複製をつくる
  • スキーマという概念が無い。なので、事前に定義したテーブルの型に縛られるようなことなく好きな形のデータを格納できる。
  • SQLでやりとりするとかはできない

DatastoreとのやりとりはQuotaと、ひいては課金と深く関わってくるので注意したほうがいいけど、今回はローカルでしか実行しないつもりなのでひとまずその辺は無視する。

サーバーサイド

以前、APIにとって重要な点はインタフェースが変わらないことである! と偉そうなことを言いながら、早々にAPIを変えることにした。といっても変わったのはURLだけだけど。どうもsendだと他のAPIと統一感が無い点が気になった。ということで、前回のsendからpostに変更して、更にコメントの格納処理を追加したコードが以下。

package hello

import (
    "fmt"
    "http"
    "template"
    "time"
    "appengine"
    "appengine/datastore"
)

const (
    GET string = "GET"
    POST string = "POST"
    DELETE string = "DELETE"
    REMARK_FIELD string = "remark"
    REMARK_KIND string = "Remark"
)

type Remark struct {
    Content string // the remark itself
    Time datastore.Time // the time the remark arrived
}

var (
    errorTemplate  = template.MustParseFile("error.html", nil)
)


func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/post", errorHandler(postRemark))
}

func root(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "At root!")
}

// Handle a remark that was sent
func postRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    
    switch r.Method {
    case POST:
        r.ParseForm()
        remark, present := r.Form[REMARK_FIELD]
        if !present { // required field does not exist
            msg := "Required field does not exist\n"
            c.Infof(msg)
            http.Error(w, msg, http.StatusBadRequest)
        } else { // Got required field
            c.Infof("Remark: %s\n", remark)

            // Store remark to datastore
            
            // Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)
            if err != nil {
                http.Error(w, err.String(), http.StatusInternalServerError)
            }

            c.Infof("Stored remark as id: %d", intID)
        }
    default:
        msg := "Invalid request\n"
        c.Infof(msg)
        http.Error(w, msg, http.StatusBadRequest)
    }
}

// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                http.Error(w, "Unknown error", http.StatusInternalServerError)
                errorTemplate.Execute(w, err)
            }
        }()
        fn(w, r)
    }
}

上のコードのなかでも、特にキモとなるのは以下の部分。

// Since we only have a single thread now, we specify a constant string as the kind.
            // In the future, we may use a unique string that is tied to the thread.
            kind := REMARK_KIND
            
            currentTime := time.Seconds() // now
            store := Remark {
                Content : remark[0],
                Time : datastore.SecondsToTime(currentTime), 
            }

            // In the future, we may assign a key for each thread,
            // and give that key as parent for each remark in that thread.
            var parentKey *datastore.Key = nil
            var stringID string = "" // using the intID, so we set an empty string for the stringID
            intID := currentTime // using the current time as an int ID
            key := datastore.NewKey(kind, stringID, intID, parentKey)
            _, err := datastore.Put(c, key, &store)

処理の内容は下の感じ。

  1. datastore格納用の構造体にデータをつめる
  2. datastore格納に必要な一意の鍵をつくる
  3. 鍵とデータを一緒に格納する

鍵の生成が少しとまどうところで、調べた感じ基本的には2種類のIDが必要だということがわかった。一つはkindという文字列、あとは数値のIDか文字列のIDのどちらか(もしくは両方)を用意しないといけない。

kindは格納する情報が持つタグのようなもので、後程データを取得するクエリを書くときにもkindを指定することになる。今回は想定しているスレッドが一つだけなので固定の文字列だけど、将来的に複数のスレッドごとにコメントを管理するとなったら(例えば)そのスレッドのタイトルをkindとして指定するとクエリが出しやすいはず。

もう一つの文字列ID、あるいは数値IDは文字通りのIDで、上のサンプルでは現在時刻をそのコメントの数値IDとして設定している。

ちなみに鍵には親鍵を指定することもできて、将来的にスレッドごとにコメントを格納して管理するとなった場合、そのスレッドの鍵を各コメントの親鍵として設定しておくとやっぱりクエリが出しやすいと思う。まだ試してはいないけど。

クライアントサイド

クライアント側は、前回のソースのうちPOSTする先のURLをhttp://local.../sendからhttp://local.../postに変更すればそのまま使える。

サーバーサイド実行結果

さてこの状態で実行をすると、下のような結果が返ってくる。ログを見る限りではちゃんと格納できてるっぽいけど、ちゃんと確認するには実際に取得できるかをみないといけない。ということで次回はその取得する部分を書いていく。

INFO     2011-06-26 02:43:01,856 __init__.py:324] building _go_app
INFO     2011-06-26 02:43:02,649 __init__.py:316] running _go_app
2011/06/26 02:43:02 INFO: Remark: [aardvark]
2011/06/26 02:43:02 INFO: Stored remark as id: 1309056182
INFO     2011-06-26 02:43:02,760 dev_appserver.py:4217] "POST /1/remark/post HTTP/1.1" 200 -
(続く)

2011年6月21日火曜日

Creating an original REST API using Google App Engine and Go (3)

2011/7/23追記: r58.1に伴うAPIの変更部分を修正

だらだらと記事を書いている間にGoogle App Engine SDKの1.5.1がリリースされて、GoでもChannel APIがサポートされるようになったみたい。まぁそれはおいおいやっていくとして、とりあえず話を続ける。

サーバーサイドのサンプル

前回"http://localhost:8080/1/remark/send"に対してPOSTするAPIをとりあえず作ってみよう、ということになったので、早速サーバーサイドからコードを書いてみる。エラーハンドリングの部分はmostachioのサンプルから引っ張ってきてるので詳しくはそちらを参照。

package hello

import (
    "fmt"
    "http"
    "template"
    "appengine"
)

const (
    GET string = "GET"
    POST string = "POST"
    DELETE string = "DELETE"
    REMARK_FIELD = "remark"
)

var (
    errorTemplate  = template.MustParseFile("error.html", nil)
)

func init() {
    http.HandleFunc("/", errorHandler(root))
    http.HandleFunc("/1/remark/send", errorHandler(sendRemark))
}

func root(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "At root!")
}

// Handle a remark that was sent to the service
func sendRemark(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)

    switch r.Method {
    case POST:
        r.ParseForm()
        remark, present := r.Form[REMARK_FIELD]
        if !present { // required field does not exist
            c.Infof("Required field does not exist\n")
            w.WriteHeader(http.StatusBadRequest)
        } else { // Got required field
            c.Infof("Remark: %s\n", remark)

            // process the remark here...
        }
    default:
        w.WriteHeader(http.StatusBadRequest)
        c.Infof("Invalid request\n")
    }
}


// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                w.WriteHeader(http.StatusInternalServerError)
                errorTemplate.Execute(w, err)
            }
        }()
        fn(w, r)
    }
}

キモはsendRemark関数の中で、処理としては以下のことをやっている。

  1. POSTかどうかを判定する。POSTじゃなかったらエラー。
  2. POSTだったら、"remark"というフィールドがちゃんと送られてるか確認する。フィールドが無ければエラー。
  3. remarkとして送られてきた内容を取得して、処理を行う。いまはまだログとして出力してるだけ。

クライアントサイドのサンプル

サーバーサイドの原型ができたところで、今度はクライアントサイドも作ってみる。こっちは別にGoでなくてもいいんだけど、せっかくなのでGoで書く。

package main

import (
    "http"
    "fmt"
)

func main() {
    client := new(http.Client)
    url := "http://localhost:8080/1/remark/send"
    data := http.Values { 
        "remark": {"aardvark", },
        "blah": {"blah", },
    }
    r, err := client.PostForm(url, data)
    if err != nil {
        fmt.Printf("Error: %s",err)
    }
    fmt.Println(r.Status)
}

内容としては、APIとして用意したURLに対して適切なフィールドと一緒にPOSTする、という極めて単純なコード。

出力結果

ということでサーバーをあげてクライアントを実行したときの両方の出力結果を見てみる。

サーバーサイドの出力

INFO     2011-06-21 14:01:03,593 __init__.py:324] building _go_app
INFO     2011-06-21 14:01:04,390 __init__.py:316] running _go_app
2011/06/21 14:01:04 INFO: Remark: [aardvark]
INFO     2011-06-21 14:01:04,501 dev_appserver.py:4217] "POST /1/remark/send HTTP/1.1" 200 -

クライアントサイドの出力

$ ./client 
200 OK
$

remarkフィールド付きの適切なPOSTを送ったので、期待通りの結果が返ってきている。ここでもしremarkの部分が存在しなかったとしたら以下の感じになる。

サーバーサイドのエラー出力

2011/06/21 14:19:49 INFO: Required field does not exist
INFO     2011-06-21 14:19:49,131 dev_appserver.py:4217] "POST /1/remark/send HTTP/1.1" 400 -

クライアントサイドのエラー出力

$ ./client 
400 Bad Request
$

期待通り、ちゃんとエラーが返ってきている。

次のステップ

そんな感じで、POSTされた発言を取りだすところまではひとまずできた。次は実際に発言を処理するところをやっていきたい。

余談

Google App Engine SDK for Go 1.5.1にしたところ、LoggingのAPIが変わったみたいで早速コンパイルエラーが出てた。

新API

func Logger(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Infof("Requested URL: %v", r.URL)
}

旧API

func Logger(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Logf("Requested URL: %v", r.URL)
}

(続く)

2011年6月12日日曜日

Creating an original REST API using Google App Engine and Go (2)

前回で簡単な動機付けを行ったので、今回はもう少しAPIの詳細に入っていく。

APIの設計

現実に利用されているRESTなAPIというと色々あるけど、ひとまず一番実績のありそうなTwitterを参考にすることにする。というわけでここの資料を読む。なんか色々書いてあるけど、とりあえず現時点で知りたいのは1点だけ。

  • そもそもURLはどういう風になっているのか?

APIにとって重要な点の一つはインタフェースが変わらないこと、変わるとしても以前のAPIがそのまま使用できる互換性を維持することなので、その辺Twitterがどうやってるのかを見たい。

で実際いくつかのAPIのURLを持ってきた。

  • http://api.twitter.com/version/statuses/public_timeline.format
  • http://api.twitter.com/1/users/show.format
  • http://api.twitter.com/1/friendships/create/id.format

見ると、URLは基本的に以下の要素で構成されていることがわかる。

  1. ルートドメイン(http://api.twitter.com/のあたり)
  2. APIのバージョン番号(/1/のあたり)
  3. APIの種類(/statues/とか/users/のあたり)
  4. 実際のコマンド(/public_timelineとか/create/idのあたり)
  5. サーバーに要求しているレスポンスの種類(formatのあたり。実際に入るのはxmlとかrssとかの拡張子)

APIのバージョン番号を直接URLに埋め込んで互換性を維持している点が特徴と言えば特徴で、わりと合理的な感じ。ということでこの構成をそのまま拝借することにする。

想定するサービス

APIを作るにはそもそもどういうサービスを作るかを想定しなきゃならない。今回はサンプルなので、シンプルに以下のようなサービスを想定する。ブログのコメント欄のようなもの、といえばわかりやすいか。

  • 匿名な利用者が発言を書きこめる。
  • 書き込まれた発言は時系列順に誰でも閲覧することができる。

これだけ。

発言を送るためのAPI

サービスの細かな仕様は作りながら考えていくとして、ひとまず発言をサーバーへ送るためのAPIを設計する。URLは先ほどの構成を拝借して、以下のような形にする。ちなみに、見ればわかるけどサーバーはローカルで動いてるものを対象としている。

  • http://localhost:8080/1/remark/send

このURLに対してPOSTすると、発言がサーバーサイドで蓄積される、ということ。

(続く)

2011年6月5日日曜日

Creating an original REST API using Google App Engine and Go

動機

今後のウェブサービスは何らかのAPIを公開するのが必須な気がしている。それは自分のサービスへ外部の開発者を呼び込む意味でもそうだし、そもそもサービスへの窓口をAPIに限定する方が昨今の状況に合っている気がする。結局のところ、ブラウザ上からサービスへアクセスするのは手段の一つに過ぎないわけで、いまはブラウザ以外にもスマートフォン向けのアプリからのアクセスも最初から考慮する必要がある。とすると、ブラウザからの表示を前提としたガチガチに密結合なサービスを作るより、APIだけを設計してブラウザだろうがアプリだろうが共通の窓口から通るようにした方が嬉しいはず。テストとか自動化できるし。クライアントのバグとサーバーのバグを明確に区別できるし。

そんな感じで納得したところで、いざAPIを設計しようとしたらどうすればいいのか良くわからない。なので、とりあえずTwitterのAPIとかを参考にしながら試しに作っていくことにする。

RESTなAPIとStreamingなAPI

TwitterのAPIを見ると、APIが主に2種類あることがわかる。一つはREST APIで、もう一つがStreaming API。これはどう違うかというと、サーバーとクライアント間の通信方法が違う。いや厳密に言うとどちらのAPIもHTTPで通信していることには変わりないんだけど、RESTなAPIがリクエスト->リプライ->別のリクエスト->リプライ…という風に何度も通信をつなぎなおしながらやりとりするのに対し、StreamingなAPIは一旦通信をつないだあとは基本的に通信を切らずにやりとりを行う。

Streamingな方式がサーバー側にとって嬉しい点は2つほどあって、一つはサーバー側からクライアント側へ向けて通知(Push)することができるということ、あともう一つはいちいち接続をつなぎなおす手間が無くなるので、わりと負荷が軽くなること。(余談だけど、Googleが以前発表したSPDYというプロトコルもこの「一度つないだらつなぎなおさないでやりとりする」という理念の下に設計されてて、SPDY使ったほうが全然速いよー、という主張もその辺に関係しているはず)

ということで、基本的にはStreamingなAPIには優位な点があるけど、でもシンプルなREST APIもやっぱり必要だよね、というざっくりとした理解で大丈夫。そもそもTwitter並に頻繁にやりとりが発生するサービスはともかく、そんなにやりとりしないサービスならRESTで十分なはず。

Google App Engine上でのAPI

Google App Engine上の話に置きかえてみる。RESTなAPIは問題無くできるとして、じゃぁStreamingなAPIはどうかというと、これができる。多分。多分というのは、自分で検証したわけでは無いから多分。

具体的には、Channel APIというのを使えばStreamingなAPIを提供できるようになるっぽい。ただ。現状はGo版のApp Engine SDKがChannel APIをまだサポートしてないので、ひとまず考えないことにする。

さて前置きがすんだところで、次回以降では実際にRESTなAPIを作っていく。

(続く)

2011年5月29日日曜日

Trying Google App Engine SDK for Go (2)

パニクったらどうなるの

moustachioのサンプルを見ると、ハンドル関数uploadを登録するときにerrorHandlerという関数(関数を返す関数)でラップした上で登録している。これはもしuploadのなかでpanicが起きたときに適切なエラーハンドルをするための措置みたい。

errorHandlerの用法

func init() {
        http.HandleFunc("/", errorHandler(upload))
        ...
}

...
 
// errorHandler wraps the argument handler with an error-catcher that
// returns a 500 HTTP error if the request fails (calls check with err non-nil).
func errorHandler(fn http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
                defer func() {
                        if err, ok := recover().(os.Error); ok {
                                w.WriteHeader(http.StatusInternalServerError)
                                errorTemplate.Execute(w, err)
                        }
                }()
                fn(w, r)
        }
}

わざわざこういうエラーハンドラが存在する必要が良くわからなかったので、このエラーハンドルを通さず意図的にpanicを起こしたらどうなるかを見てみた。

サンプル

package hello

import (
    "fmt"
    "http"
)

func init() {
    http.HandleFunc("/", root)
}

func root(w http.ResponseWriter, r *http.Request) {
    // test panic here to see what happens
    panic("Testing panic")

    ch := make(chan string, 1)
    go func(){
        ch <- "aaaaa!!"
    }()
    fmt.Fprint(w, "At Root! ", <- ch)
}

実行結果

どうやらデフォルトのエラーハンドラが動いて、ブラウザ上はInternal Server Errorが発生したことが伝わるみたい。ただ、クライアントに対して適切なエラーコードが送られたかどうかは定かでない。

スクリーンショット

dev_appserverのログ

$ dev_appserver.py myapp/
Warning: You are using a Python runtime (2.7) that is more recent than the production runtime environment (2.5). Your application may use features that are not available in the production environment and may not work correctly when deployed to production.
INFO     2011-05-29 02:42:43,751 appengine_rpc.py:159] Server: appengine.google.com
INFO     2011-05-29 02:42:43,754 appcfg.py:440] Checking for updates to the SDK.
INFO     2011-05-29 02:42:44,311 appcfg.py:457] The SDK is up to date.
WARNING  2011-05-29 02:42:44,311 datastore_file_stub.py:657] Could not read datastore data from /tmp/dev_appserver.datastore
INFO     2011-05-29 02:42:44,313 rdbms_sqlite.py:58] Connecting to SQLite database '' with file '/tmp/dev_appserver.rdbms'
INFO     2011-05-29 02:42:44,366 dev_appserver_multiprocess.py:637] Running application xclamm on port 8080: http://localhost:8080
INFO     2011-05-29 02:42:52,748 __init__.py:284] building _go_app
INFO     2011-05-29 02:42:53,503 __init__.py:276] running _go_app
panic: Testing panic

runtime.panic+0xa4 /tmp/appengine/google_appengine/goroot/src/pkg/runtime/proc.c:1060
 runtime.panic(0x811c8e4, 0x9762b550)
hello.root+0x4e hello/panic.go:14
 hello.root(0x976a54a0, 0x9762cf60)
http.HandlerFunc·ServeHTTP+0x35 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:533
 http.HandlerFunc·ServeHTTP(0x805b7f0, 0x976a54a0, 0x9762cf60, 0x976bc000, 0x805b7f0, ...)
http.*ServeMux·ServeHTTP+0x194 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:728
 http.*ServeMux·ServeHTTP(0x9762b2f0, 0x976a54a0, 0x9762cf60, 0x976bc000, 0x9762cf60, ...)
http.*conn·serve+0x202 /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:499
 http.*conn·serve(0x9762ce40, 0x0)
runtime.goexit /tmp/appengine/google_appengine/goroot/src/pkg/runtime/proc.c:178
 runtime.goexit()
----- goroutine created by -----
http.*Server·Serve+0x1cf /tmp/appengine/google_appengine/goroot/src/pkg/http/server.go:820
(後略)

まとめ

一応デフォルトのエラーハンドラは存在するみたい。でもやっぱり自分でハンドルしたほうがいいね。

  • Go版のGoogle App Engineには(一応)デフォルトのエラーハンドラがいる
  • クライアントに適切なエラーコードを返してるかは定かでない
  • どっちにしろ見た目がしょぼいから自分でエラーハンドルしたほうがいい

2011年5月17日火曜日

Trying Google App Engine SDK for Go

Google App EngineがGoに対応したとかで、とりあえず試してみることにした。

インストール

何はともあれSDKをダウンロードしてきてパスを通す。

$ cd ~/lib
$ wget http://googleappengine.googlecode.com/files/go_appengine_sdk_linux_386-1.5.0.zip
$ unzip go_appengine_sdk_linux_386-1.5.0.zip
$ export PATH=~/lib/google_appengine/:$PATH

サンプルの作成と実行

サンプルの作成手順は本家ドキュメントに任せるとして、ここでは実際の実行結果を見ていく。

$ dev_appserver.py myapp/
Warning: You are using a Python runtime (2.6) that is more recent than the production runtime environment (2.5). Your application may use features that are not available in the production environment and may not work correctly when deployed to production.
INFO     2011-05-17 14:17:10,225 appengine_rpc.py:159] Server: appengine.google.com
INFO     2011-05-17 14:17:10,229 appcfg.py:440] Checking for updates to the SDK.
INFO     2011-05-17 14:17:10,550 appcfg.py:457] The SDK is up to date.
WARNING  2011-05-17 14:17:10,550 datastore_file_stub.py:657] Could not read datastore data from /tmp/dev_appserver.datastore
INFO     2011-05-17 14:17:10,552 rdbms_sqlite.py:58] Connecting to SQLite database '' with file '/tmp/dev_appserver.rdbms'
INFO     2011-05-17 14:17:10,598 dev_appserver_multiprocess.py:637] Running application helloworld on port 8080: http://localhost:8080

これでGoogle App Engineのサーバーがローカルで起動したので、あとはブラウザからhttp://localhost:8080/にアクセスすると、"Hello, World!"と記載されただけのページが表示される。それに併せて、dev_appserver側では以下のログが出てくる。そのログを見ると、実行時にGoのソースをコンパイルして即実行してるのがわかる。

INFO     2011-05-17 14:17:36,341 __init__.py:284] building _go_app
INFO     2011-05-17 14:17:37,011 __init__.py:276] running _go_app
INFO     2011-05-17 14:17:37,080 dev_appserver.py:4200] "GET / HTTP/1.1" 200 -
INFO     2011-05-17 14:17:37,139 dev_appserver.py:4200] "GET /favicon.ico HTTP/1.1" 200 -
INFO     2011-05-17 14:17:40,245 dev_appserver.py:4200] "GET /favicon.ico HTTP/1.1" 200 -

2011年4月16日土曜日

Go fix that **** code...

Goのユーザーとしてちょっと辟易していたこととして、開発チームがGoそのものの文法や標準ライブラリのAPIをガツガツ変更することが挙げられる。いやホントに良く変わる。発表されてからもう1年経ったけど、まだ当分安定するようには見えない。

で変更されて何が困るかというと、古いコードのコンパイルが通らなくなること。自分が書いたコードならいざしらず、他人が書いたコードのコンパイルが通らなくなってしまうと結構厄介。メンテナー次第では下手すると当分コードがアップデートされずにそのままなんてこともありうる。じゃあ自分で直そうにも、もしかしたらエンバグするかもしれないし、そもそもが面倒だからあんまりヤル気にならない。そうすると自分が使ってたパッケージが使えなくなって、でもかといって古いバージョンのGoを使う気にもなれなくて、開発に対するモチベーションが下がって…という酷い悪循環におちいる。自分は。

Gofix

そういう状況を開発チームも理解していたのか、ちょっと前にGofixなんていうツールが発表された。詳しくはRussの書いたブログを読んでもらうとして、Gofixがやってくれるのは簡単に言うと「コードの自動修復」だ。どうやら古いAPIやら文法が使われているコードを自動的に修復して、ナウいバージョンに仕立てなおしてくれるらしい。

Go fix Thrift

ということで、実益と実験も兼ねてThriftのGoコードをGofixにかける実験をやってみた。なお使用したバージョンはGoがr8131、Thriftがr1093950。

Thrift修正前

Thriftの修正前ライブラリを普通にコンパイルしようとしたら、以下のようになる。net.DialあたりのAPIが変わったせいでコンパイルが通らない。

$ make
cd thrift/ && gomake install
make[1]: Entering directory `/home/masato/lib/thrift/lib/go/thrift'
8g -o _go_.8 tapplication_exception.go tbase.go tbinary_protocol.go tcompact_protocol.go tcompare.go tcontainer.go texception.go tfield.go tframed_transport.go thttp_client.go tiostream_transport.go tlist.go tjson_protocol.go tmap.go tmemory_buffer.go tmessage.go tmessagetype.go tnonblocking_server.go tnonblocking_server_socket.go tnonblocking_socket.go tnonblocking_transport.go tnumeric.go tprocessor.go tprocessor_factory.go tprotocol.go tprotocol_exception.go tprotocol_factory.go tserver.go tserver_socket.go tserver_transport.go tset.go tsimple_server.go tsimple_json_protocol.go tsocket.go tstruct.go ttransport.go ttransport_exception.go ttransport_factory.go ttype.go
tnonblocking_socket.go:111: too many arguments in call to net.Dial
tsocket.go:137: too many arguments in call to net.Dial
make[1]: *** [_go_.8] Error 1
make[1]: Leaving directory `/home/masato/lib/thrift/lib/go/thrift'
make: *** [thrift/.install] Error 2

Thrift修正後

この状況から、まずGofixを走らせた上でmakeすると…

$ gofix .
thrift/tnonblocking_socket.go: fixed netdial
thrift/tsocket.go: fixed netdial
$ make
cd thrift/ && gomake install
make[1]: Entering directory `/home/masato/lib/thrift/lib/go/thrift'
8g -o _go_.8 tapplication_exception.go tbase.go tbinary_protocol.go tcompact_protocol.go tcompare.go tcontainer.go texception.go tfield.go tframed_transport.go thttp_client.go tiostream_transport.go tlist.go tjson_protocol.go tmap.go tmemory_buffer.go tmessage.go tmessagetype.go tnonblocking_server.go tnonblocking_server_socket.go tnonblocking_socket.go tnonblocking_transport.go tnumeric.go tprocessor.go tprocessor_factory.go tprotocol.go tprotocol_exception.go tprotocol_factory.go tserver.go tserver_socket.go tserver_transport.go tset.go tsimple_server.go tsimple_json_protocol.go tsocket.go tstruct.go ttransport.go ttransport_exception.go ttransport_factory.go ttype.go
rm -f _obj/thrift.a
gopack grc _obj/thrift.a _go_.8
cp _obj/thrift.a "/home/masato/lib/google-go/pkg/linux_386/thrift.a"
make[1]: Leaving directory `/home/masato/lib/thrift/lib/go/thrift'

こんな感じで上手く行く。スバラシイ。

Thrift修正差分

ついでにどういう差分が適用されたかを見てみる。下はdiffをとった結果の一部だけど、net.Dialのところに注目するとちゃんと引数が減らされてる。

-  var err os.Error
-  if p.conn, err = net.Dial(p.addr.Network(), "", p.addr.String()); err != nil {
-    LOGGER.Print("Could not open socket", err.String())
-    return NewTTransportException(NOT_OPEN, err.String())
-  }
-  if p.conn != nil {
-    p.conn.SetTimeout(p.nsecTimeout)
-  }
-  return nil
+       var err os.Error
+       if p.conn, err = net.Dial(p.addr.Network(), p.addr.String()); err != nil {
+               LOGGER.Print("Could not open socket", err.String())
+               return NewTTransportException(NOT_OPEN, err.String())
+       }
+       if p.conn != nil {
+               p.conn.SetTimeout(p.nsecTimeout)
+       }
+       return nil

まとめ

ということで、Thriftという実例をつかってGofixの便利さを体感してみた。こういう便利ツールが公式に提供されて、かつほとんどが自動的に修正されるのであればAPIやら文法の変更もバンバンやってくれて構わないかも。

ちなみに、Thriftが生成したCassandraへのインタフェースライブラリをGofixしてもコンパイルが通らなかったことに関しては、また別の話。これThriftのバグなのか…? いまいち追う気になれない。

2011年4月9日土曜日

OCaml Pattern Matching

引きつづきOCaml。今度はパターンマッチング。パターンマッチング自体はその名の通り、与えられた引数の値に応じた値を返す関数を定義する仕組み。

組込み型でのパターンマッチング

(* normal pattern matching *)
let color_to_num = function
    "RED" -> 0
  | "BLUE" -> 1
  | "GREEN" -> 2
  | _ -> -1;;

let num = color_to_num "RED" in
Printf.printf "%d\n" num;;

let num = color_to_num "aardvark" in
Printf.printf "%d\n" num;;

出力

$ ./a.out
0
-1

独自型でのパターンマッチング

(* matching via Sum type*)
type color_t = Red | Blue | Green ;;
let color_t_to_num = function
    Red -> 0
  | Blue -> 1
  | Green -> 2;;

let num = color_t_to_num Blue in
Printf.printf "%d\n" num;;

(* Compile error
let num = color_t_to_num Black in
Printf.printf "%d\n" num;;

let num = color_t_to_num "aardvark" in
Printf.printf "%d\n" num;;
*)

出力

$ ./a.out
1

やっぱ型チェックが厳しいと安心してコードがかけるのがいい。正直Pythonとかはもうあまり書きたくないかも。

パターンマッチングの文法に関しては、let xxxx = function ... という書きかたと let xxxx = match x with ... という書きかたの使いわけが良くわからない。ただ、Oreillyの本で読むかぎりだとfunctionを使う方は単なるシンタックスシュガーっぽい。

2011年4月3日日曜日

Object Oriented OCaml

引きつづきOCaml。OCamlのObject Orientedな部分の仕様が思ってたより膨大で、Thriftが生成したコードが理解できないのでOreillyの本を読みつつサンプルを書いてみる。

oo.ml

class car (name, color)=
  object
      val mutable m_name = name
      val mutable m_color = color
      method get_name = m_name
      method get_color = m_color
      method to_string = m_color ^ " " ^ m_name
  end;;

class flying_car(name, color, flying) =
  object
      inherit car(name, color)
      val mutable m_flying = flying
      method land_car = m_flying <- false
      method fly_car = m_flying <- true
      method to_string =
        let state = if m_flying then "flying" else "not flying" in
        state ^ " " ^ m_color ^ " " ^ m_name
  end;;

let c = new flying_car("Keitora", "black", true) in
let () = c#land_car in
let msg = "I bought a new " ^ c#to_string in
print_endline msg

出力

$ ocamlc oo.ml
$ ./a.out
I bought a new not flying black Keitora

しかしOOな仕様が入ると途端に予約語が増える。ちなみに、最初は関数land_carをlandって名前にしてて謎のコンパイルエラーが発生してた。結局原因はlandという名前の組込み演算子が存在するから。syntax errorってだけじゃなかなか気付かないッスよコンパイラさん。

2011年3月27日日曜日

Konnichiwa OCaml

Goの言語仕様が変わって周辺ライブラリのコンパイルが通らなくなったので、とりあえずGoは放置してOCamlでThriftを試すことにした。その前段階としてOCamlの適当なサンプルでも書いてみる。

OCaml自体は結構前から知ってたけど、使う機会も無いしなんとなく書きかたが気持ちわるかったから敬遠してた。まあこの際だから試そう。

環境のセットアップ

Ubuntuならラクチン。

$ sudo apt-get install ocaml ocaml-mode

hello.ml

(* util functions *)
let concat (x, y) = 
  x ^ " " ^ y;;

(* main function *)
let msg = "konnichiwa" in
let name = "aardvark" in
let con = concat(msg, name) in
    print_endline con ;;

Makefile

コンパイルすると*.cmiというインタフェースの情報を記述したファイルと*.cmoというオブジェクトファイルができるけど、単純に実行バイナリを作るのに必要なのは*.cmoだけっぽい。実際、ソースをコンパイルしたあとにできる*.cmiファイルを消して、そのうえでリンクしても問題無かった。

BIN=hello
SRC=hello.ml
CAMLC=ocamlc
OBJ=$(SRC:.cmo=.ml)

$(BIN): $(OBJ)
 $(CAMLC) -o $(BIN) $(OBJ)

.ml.cmo:
 $(CAMLC) -c $<

clean:
 rm -f *.cm[iox] *~ .*~ #*#
 rm -f $(BIN)

出力

$ make
ocamlc -o hello hello.ml
$ file hello.cmi
hello.cmi: OCaml interface file (.cmi) (Version 011)
$ file hello.cmo
hello.cmo: OCaml object file (.cmo) (Version 007)
$ ./hello
konnichiwa aardvark

末尾のセミコロン二つがちょっと気持ち悪い。

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
    ...
    

(続く)