ラベル Go の投稿を表示しています。 すべての投稿を表示
ラベル Go の投稿を表示しています。 すべての投稿を表示

2013年12月7日土曜日

Skeleton Project for Google App Engine/Go + Dart

2016/4/30更新

この記事の内容はすでに古く、またGithub上にリポジトリにおいてあるコードも最新のDartに合わせて変わっているため参考にならない点に注意。

Dartも1.0になったことだしそろそろ試す人が増えてきてもいいはず。ということで、Google App Engine上でDartなウェブアプリを作りたい人のためにスケルトンのプロジェクトを作った。バックエンドの言語はGoでフロントエンドはDart。動作確認はDartiumでしかしてないので、他のブラウザで動かしたい人はどっかでdart2js通せばいける…はず? 正直dart2jsほとんど試したことないからよくわからない。

スケルトンプロジェクトについて

Github上にリポジトリを作ったのでそれをCloneするなりForkするなり好きにやってもらえればいいはず。

ちなみに今回は実際にApp Engine上にデプロイしてあるので、そっちでどんなもんか確認することもできる。URLはgae-go-dart-skeleton.appspot.comで、Dartiumでアクセスすればちゃんと見れるはず。なおローカルからDartium上で動かすとこんな感じ(Windows 8.1 + GAE/Go 1.8.8 + Dart SDK 1.0.0.3_r30188)。

スケルトン

スケルトンプロジェクトの動かし方

gae-go-dart-skeletonを動かすには以下の手順を踏む必要がある。

  1. GAE/GoのSDKとDartのSDKをダウンロードしてきてパスを通す
  2. git clone git@github.com:rillomas/gae-go-dart-skeleton.gitする。あるいは自分のとこにForkしてそっちをcloneする
  3. pub getして必要なパッケージを落としてくる
  4. dev_appserver.pyを使ってローカルで動かす
  5. (デプロイする場合は)appcfg.pyでアプリをアップする

pub get

pubはDartに標準でついてくるパッケージマネージャで、pubspec.yamlファイルに必要なものを書くと落としてきてくれる。今回のpubspec.yamlはこんな感じ。

dependencies:
  logging: any
  polymer: any

Polymerとログ用のパッケージしか書いてないけど、それ以外に必要なパッケージはpub側で判断してもってきてくれるからこれで十分。でpubspec.yamlが置いてあるディレクトリでpub getするとこんな感じに実行される。

$ pub get
Resolving dependencies............................
Downloading polymer 0.9.1+1 from hosted...
Downloading polymer_expressions 0.9.1 from hosted...
Downloading shadow_dom 0.9.1 from hosted...
Downloading observe 0.9.1+1 from hosted...
Downloading html5lib 0.9.1 from hosted...
Got dependencies!

ルートディレクトリにpackagesってディレクトリが作られて、そこにパッケージがすべてダウンロードされてくるはず。あとは必要なところにpackagesへのリンクが自動的に張られる。それ以外にもpubspec.lockていうファイルが作られるけど、それはpubがバージョンの管理用に自動生成するファイルなのであまり気にしなくて大丈夫。

App Engine関係

App Engine関係は公式ドキュメントがあるからそっちでよろしく。dev_appserver.pyまわりはここ、appcfg.pyはこことか。

コードについて

今回のはPolymerを使った極めて単純なウェブアプリなんだけど、クライアントサイドで完結するようなアプリだと面白くないのでサーバー側のREST APIを叩いてdatastoreからデータを持ってきてそれを表示している。

サーバーサイド

サーバー側は来訪者の情報を提供するためのvisitorInfo APIを用意している。パスはapi/1/visitorInfoで、そこにGETをかければjsonが返ってくるし、POSTすればサーバー側の数値を書き換えることができる。実際アップしてあるアプリのAPIにアクセスすると現在の来訪者数累計が取得できるはず。やっている処理の内容はserver/main.goの以下のあたり。

func init() {
    http.HandleFunc("/api/1/visitorInfo", handleVisitorInfoRequest)
}

func handleVisitorInfoRequest(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Debugf("Method: %s Url:%s ContentLength: %d\n", r.Method, r.URL, r.ContentLength)
    if (r.Method == "GET") {
        getVisitorInfo(c,w,r)
    } else if (r.Method == "POST") {
        setVisitorInfo(c,w,r)
    }
}

getVisitorInfo()の中でdatastoreからデータの取得とGETへの応答、setVisitorInfo()のなかでPOSTで送られてきたデータのdatastoreへの格納をやっている。datastoreへのget/setは固定の文字列キーでやってるので基本的に間違いはないはずなんだけど、初回にまだ何もsetされる前にgetが走る可能性があるのでNoSuchEntityエラーだけは現状握りつぶしてる。

const (
    visitorInfoKind string = "visitorInfo"
    visitorInfoKey string = "theVisitorInfoKey"
)

type VisitorInfo struct {
    Count int // total number of visitors
}

func getVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)
    info := new(VisitorInfo)
    err := datastore.Get(c,k, info)
    // ignore the NoSuchEntityError and return the default value if we don't have the entity stored yet
    if err != nil && err != datastore.ErrNoSuchEntity {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    outBuf, err := json.Marshal(info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    size, err := w.Write(outBuf)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    c.Debugf("Wrote %d bytes as response\n", size)

}

func setVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    buf, err := ioutil.ReadAll(r.Body)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var info VisitorInfo
    json.Unmarshal(buf, &info)
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)

    _,err = datastore.Put(c,k,&info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

クライアントサイド

クライアント側ではMainApp(index.htmlから参照されてるWeb Component)が表示された段階でサーバーに対してGETを発行して、結果をもとに自身の内容を更新している。コード的にはclient/lib/components/main_app.dartの以下のあたり。

    @override
    void enteredView() {
        super.enteredView();

        var domain = ServerChannel.generateRootDomain(window.location);
        ServerChannel.getVisitorInfo(domain).then((info) {
            info.count++;
            visitorInfo = info;

            // update the visitor info on the server side
            ServerChannel.sendVisitorInfo(info, domain);
        });
    }

enteredView()はWeb Component側がdocumentに挿入されたときに呼ばれてくる関数で、今回はそこでvisitorInfo APIへGETを投げている。サーバーから今までの来訪者数を取得して、自分の分を加算して表示、更にサーバー側の来訪者数をPOSTで更新するところまで通してやっている。

ちなみにこのやり方だと来訪者数が一瞬だけゼロ表示されてしまうので、情報がもらえるまでは表示そのものを隠すとかした方が美しいのかも。めんどうだからやってない。

実際にHttpリクエストを投げる部分はすべてServerChannelというクラスで隠蔽している。コード的にはclient/lib/services/server_channel.dart

class ServerChannel {

    static Future<VisitorInfo> getVisitorInfo(String domain) {
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("getting from ${url}");
        var task = new AsyncGet<int>();
        var f = task.request(url, (res) {
            Logger.root.info("response: $res");
            int count = -1;
            if (res.isEmpty) {
                Logger.root.warning("received empty string");
                return count;
            }
            Map map = JSON.decode(res);
            var info = new VisitorInfo.fromJson(map);
            return info;
        });
        return f;
    }

    static void sendVisitorInfo(VisitorInfo info, String domain) {
        var request = new HttpRequest();
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("Sending info to ${url}");
        request.open("POST", url);
        var str = JSON.encode(info);
        request.send(str);
    }

    // Generate the root domain from location information
    static String generateRootDomain(Location location) {
        return "${location.protocol}//${location.host}/";
    }

    static const string visitorInfoApiPath = "api/1/visitorInfo";
}

内部で使っているAsynGetていう独自クラスがFutureやらCompleterやらを駆使して非同期なコードをDartっぽくなるようにラップしてる。

class AsyncGet<Output> {
    Future<Output> request(String url, Output process(String response)) {
        HttpRequest.request(url).then((r) {
            if (r.readyState == HttpRequest.DONE &&
                (r.status == 200)) {
                Output out = process(r.responseText); // convert the raw response text
                _completer.complete(out);
            }
        });
        return _completer.future;
    }

    Completer<Output> _completer = new Completer<Output>();
}

ServerChannelを経由してサーバーから取得できた来訪者情報は、visitorInfo変数に代入された段階で自動的にバインド先に反映される。これはPolymerのおかげ。その辺のバインドが書いてあるのはMainAppのビューに相当するclient/lib/components/main_app.html。

<polymer-element name="main-app">
    <template>
        <h2>Welcome to my Dart Skeleton</h2>
        <p>You are the #{{visitorInfo.count}} visitor!</p>
    </template>
    <script type="application/dart" src="main-app.dart"></script>
</polymer-element>

自分でDOMいじる必要がないから楽。

雑感

App Engine + Dartでもわりと普通に動いてる印象。ただこっちで試したところ、Channel APIをjs-interopでラップするとデプロイしたときに"Uncaught Closure call with mismatched arguments: function 'call'"とかいう謎のエラーでうまく動かないことがわかってる。でもなぜかローカルの環境だとちゃんと動く。

DartにPolymerが無くてWeb UIだった頃から追ってるけど、Polymerは素直に書けてちゃんと動くしいいと思う。Web UIの頃はところどころ未完成でいまいちだった。個人的には@observableとかのアノテーションを書くのはちょっと違和感があるんだけど、たぶん慣れの問題。あとは早めにWeb AnimationsとPolymer UI Elementsを移植してもらえると助かるかな。

そんな感じ。Dart VMはいずれChrome stableにマージされる予定らしいし、dart2jsの性能もメリメリ向上してるのでみんなどんどんDart書くといいよ。

Dart Advent Calendar 2013 12月7日担当

2012年5月28日月曜日

WebSocket client with Dart, WebSocket server with Go

ちょっと前の話になるけど、5/12に開催されたDartのハッカソンに参加して、そこでWebSocketを用いてウェブアプリをつくるチームに参加した。自分はここで公開してるSVGアニメーション用のコードがあったので、それを用いたアプリを作ってきた。

その場ではサーバーもクライアントもDartで書いたんだけど、個人的にはサーバーサイドはGoで書きたかったので改めてGoで書きなおしてみた。ちなみにハッカソンではGithubを使って開発したので、それを自分のところへForkして拡張してる。場所はここね。

server.go

基本的にはmanageClientという関数がクライアントに関するリソースを管理するアクターで、serveClient内での各クライアントとのやりとりはすべてこのmanageClientに対して転送される。例えば新規にクライアントがつながってきた場合はAddClientRequestがserveClientからmanageClientに対して送られ、クライアント一覧に相当するmapにクライアントとの接続が格納される。

ちなみにちょっと苦労したのがscriptやらCSSやらをGoのhttpサーバー経由でどうやってホストするのか、という点。それは色々調べたところhttp.ServeFile()という関数を使うとファイルに対するリクエストは大体いい感じに処理してくれることがわかったので、下のコードはそれを採用している

package main

import (
 "code.google.com/p/go.net/websocket"
 "net/http"
 "fmt"
)

type RequestType int
type ResponseType int

var (
 requestQueue = make(chan Request)
 responseQueue = make(chan Response)
)

const (
 // Request
 GENERATE_CLIENT_ID RequestType = 0
 ADD_CLIENT RequestType = 1
 REMOVE_CLIENT RequestType = 2
 BROADCAST RequestType = 3

 // Response
 CLIENT_ID ResponseType = 0
)

type RequestParameter struct {
 clientID int
 Connection *websocket.Conn
}

type DisplayParameter struct {
 Remark string
 Duration string
 Path string
 Rotate string
}

type Request interface {
 Type() RequestType
 ClientID() int
 Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn
}

type GenerateClientIDRequest struct {
 RequestParameter
}

type BroadcastRequest struct {
 RequestParameter
 DisplayParameter
}

type AddClientRequest struct {
 RequestParameter
}

type RemoveClientRequest struct {
 RequestParameter
}

type Response interface {
 Type() ResponseType
}

type GenerateClientIDResponse struct {
 ClientID int
}

func (gcr GenerateClientIDResponse) Type() ResponseType {
 return CLIENT_ID
}

func (gcm GenerateClientIDRequest) ClientID() int {
 return gcm.clientID
}

func (gcm GenerateClientIDRequest) Type() RequestType {
 return GENERATE_CLIENT_ID
}

func (gcm GenerateClientIDRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 res := GenerateClientIDResponse {
  *currentClientID,
 }
 queue <- res
 // proceed to next id
 *currentClientID++
 return clientList
}



func (acm AddClientRequest) ClientID() int {
 return acm.clientID
}

func (acm AddClientRequest) Type() RequestType {
 return ADD_CLIENT
}

func (acm AddClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 clientList[acm.clientID] = acm.Connection
 fmt.Printf("Added client. %d clients existing.\n", len(clientList))
 return clientList
}

func (bm BroadcastRequest) ClientID() int {
 return bm.clientID
}

func (bm BroadcastRequest) Type() RequestType {
 return BROADCAST
}

func (bm BroadcastRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // send a text message serialized as JSON.
 for id, client  := range clientList {
  fmt.Printf("Sending to %d\n", id)
  err := websocket.JSON.Send(client, bm.DisplayParameter)
  if err != nil {
   fmt.Printf("error: %s\n",err)
   // remove client
   delete(clientList, id)
  }
 }
 fmt.Println("Broadcasted")
 return clientList
}

func (rcm RemoveClientRequest) ClientID() int {
 return rcm.clientID
}

func (rcm RemoveClientRequest) Type() RequestType {
 return REMOVE_CLIENT
}

func (rcm RemoveClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // remove client
 delete(clientList, rcm.clientID)
 fmt.Printf("Removed client ID:%d. %d left\n", rcm.clientID, len(clientList))
 return clientList
}

/**
  * Message loop for the client manager
  */
func manageClient(reqQueue chan Request, resQueue chan Response) {
 var currentClientID int = 0
 clientList := make(map[int]*websocket.Conn, 0)
 fmt.Println("Entering client manage loop")
 for {
  select {
  case msg := <- reqQueue:
   fmt.Printf("client:%d type:%d\n", msg.ClientID(), msg.Type())
   clientList = msg.Process(clientList, resQueue, &currentClientID)
  }
 }
}

/**
  * Get a new client ID from the client manager
  */
func generateClientID(reqQueue chan Request, resQueue chan Response)  int {
 msg := GenerateClientIDRequest {}
 reqQueue <- msg
 res := <- resQueue
 v, _ := res.(GenerateClientIDResponse)
 return v.ClientID
}

/**
  * Function that serves each client
  */
func serveClient(ws *websocket.Conn, reqQueue chan Request, resQueue chan Response) {
 id := generateClientID(reqQueue, resQueue)
 defer func() {
  // remove client when exiting
  msg := RemoveClientRequest {
   RequestParameter {
    id,
    ws,
   },
  }
  reqQueue <- msg
 }()

 // add client first
 msg := AddClientRequest {
  RequestParameter {
   id,
   ws,
  },
 }
 reqQueue <- msg
 fmt.Println("Entering receive loop")
 for {
  var param DisplayParameter
  err := websocket.JSON.Receive(ws, &param)
  if err != nil {
   fmt.Printf("receive error: %s\n",err)
   break
  }
  fmt.Printf("recv:%#v\n", param)

  // broadcast received message
  msg := BroadcastRequest {
   RequestParameter {
    id,
    ws,
   },
   param,
  }
  reqQueue <- msg
 }
}

/**
  * Handler for the websocket json server
  */
func echoJsonServer(ws *websocket.Conn) {
 fmt.Printf("jsonServer %#v\n", ws.Config())
 serveClient(ws, requestQueue, responseQueue)
}

/**
  * Serve all files (html, dart, css)
  */
func mainServer(w http.ResponseWriter, req *http.Request) {
 path := req.URL.Path[1:]
 fmt.Printf("path: %s\n",path)
 http.ServeFile(w, req, path)
}

func main() {
 // start an actor that processes requests from every client
 go manageClient(requestQueue, responseQueue)

 // setup the handlers
 http.Handle("/echo", websocket.Handler(echoJsonServer))
 http.HandleFunc("/", mainServer)
 fmt.Println("serving...")
 port := 8080
 err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
 if err != nil {
  panic("ListenAndServer: " + err.Error())
 }
}

client.dart

クライアント側で実行されるDartのコードが以下。いままでのサンプルと違うのは、他のクライアントとポスト内容を共有するためポストボタンをクリックしたら即websocketで内容を送っているところ。実際の表示処理はサーバー側からブロードキャストされてきたメッセージに対してon.messageのハンドラで実行することになる。

#import("dart:html");
#import("dart:json");
#import("RemarkDisplayer.dart");
#import("SVGRemarkDisplayer.dart");
#import("Unit.dart");

void main() {
    // initialize displayers
    final int MAX_NUMBER_OF_REMARKS = 15;
    final int MAX_SPEED = 100;
    var displayer = new SVGRemarkDisplayer();
    displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

    // create websocket and add handlers
    WebSocket webSocket = new WebSocket("ws://localhost:8080/echo");
    Element status = document.query("#statusArea");
    webSocket.on.message.add((event) {
            // display remark with given parameters
            var message = event.data;
            Map map = JSON.parse(message);
            DisplayParameter param = new DisplayParameter.map(map);
            displayer.display(param);
        });

    // connect post button with websocket
    TextAreaElement textNode = document.query("#remarkText");
    TextAreaElement pathNode = document.query("#pathString");
    Element postButton = document.query("#postRemark");
    InputElement speedNode = document.query("#displaySpeed");
    InputElement rotateNode = document.query("#rotateRemark");
    postButton.on.click.add((Event e) {
        String message = textNode.value;
        String path = pathNode.value;

        bool rotate = rotateNode.checked;
        var num = speedNode.valueAsNumber;
        var duration = (MAX_SPEED - num.toInt()) * 100;
        var param = new DisplayParameter(message, duration, Unit.Millisecond, path, rotate);
        String paramJson = JSON.stringify(param.toMap());
        webSocket.send(paramJson);
    });
}

感想

Dartで書いたときと比べてGo版はかなりコード量が増えた気がする。大半はmanageClientとどういったメッセージをやりとりするかの定義なので、アクターモデルをやるからにはしょうがないのかもしれない。ただやっぱり厳密に書こうとするとアクターモデルはめんどう。もっと規模が大きくなればアクターモデルの恩恵がわかる、かも?

サーバー側の出力例

サーバー側をビルド・実行して、上記のDartクライアントからGoogleのアスキーアートをPostした場合の出力が下のログ。参考に掲載しておく。

$ go build server.go 
$ ./server 
serving...
Entering client manage loop
path: 
jsonServer &websocket.Config{Location:(*url.URL)(0x188abd80), Origin:(*url.URL)(0x188abdc0), Protocol:[]string(nil), Version:13, TlsConfig:(*tls.Config)(nil), handshakeData:map[string]string(nil)}
client:0 type:0
Entering receive loop
client:0 type:1
Added client. 1 clients existing.
path: favicon.ico
recv:main.DisplayParameter{Remark:"\u3000\u3000\u3000\u3000_,,,,._\u3000\u3000\u3000\u3000\u3000 \u3000 \u3000 \u3000 \u3000 \u3000 \u3000 、-r\n\u3000 \u3000,.','\" ̄`,ゝ\u3000_,,,_\u3000\u3000 _,,,_\u3000\u3000 _,,,,__,. | |\u3000 _,,,,,_\n\u3000\u3000{ {\u3000\u3000 ,___\u3000,'r⌒!゙! ,'r⌒!゙! ,.'r⌒!.!\"| l ,.'r_,,.>〉\n\u3000\u3000ゝヽ、\u3000~]|\u3000ゞ_,.'ノ\u3000ゞ_,.'ノ\u3000ゞ__,.'ノ\u3000| l {,ヽ、__,.\n\u3000\u3000\u3000`ー-‐'\"\u3000\u3000 ~\u3000\u3000\u3000 ~\u3000\u3000〃 `゙,ヽ ̄` `゙'''\"\n\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000 ゙=、_,.〃 ", Duration:"5000&ms", Path:"M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120 A15 10 170 0 1 10 150", Rotate:"false"}
client:0 type:3
Sending to 0
Broadcasted

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

(続く)

2010年11月15日月曜日

GDD 2010 dev quizのしりとりを解く(はずの)Goコード

ちょっと前GDD 2010のdev quiz用に書いたGoのコードを公開してみる。当日は出張で出席できないことがわかってたので、実際に申し込んではいないからあってるかどうかは定かでない。とりあえずLevel 2まではそれっぽい答が出てるのは確認した。Level 3は時間がかかりすぎるパスを先に切らないと常識的な時間で終わらなかったはず。

ちなみに、元々はPythonで書いたのをGoに書き直したコードだったりする。実行時間的には4分かかってた処理が20秒くらいに縮まった。

コード


package main
import (
    "flag"
    "os"
    "log"
    "bufio"
    "fmt"
    "strings"
    "container/vector"
)

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

func ExtractFirstAndLastLetter(word string) string {
    first := word[0]
    last := word[len(word)-1]
    return fmt.Sprintf("%c%c", first, last)
}

var CHAR_LIST string = "abcdefghijklmnopqrstuvwxyz"

type StringList struct {
    list vector.StringVector
}

func (l *StringList) add(s string) {
    l.list.Push(s)
}

func (l *StringList) copy() StringList {
    copied := l.list.Copy()
    return StringList { copied }
}

func (l *StringList) delete(index int) {
    l.list.Delete(index)
}

func (l *StringList) size() int {
    return l.list.Len()
}


func (l *StringList) join(sep string) string {
    return strings.Join(l.list, sep)
}

type WordDictionary map[byte] StringList

func (d WordDictionary) copy() WordDictionary {
    newDict := make(WordDictionary)
    for key,val := range d {
        newDict[key] = val.copy()
    }
    return newDict
}

const (
    WIN = 0
    LOSE = 1
)

func DoShiritoriToEnd(dict WordDictionary, word string, wordPath StringList, depth int) {
    depth += 1
    endIndex := len(word)-1
    lastChar := word[endIndex]
    wordList := dict[lastChar]
    if wordList.size() == 0 {
        joined := wordPath.join("-")
        winLose := depth % 2
        if winLose == WIN {
            logger.Print(" WIN:"+joined)
        } else {
            logger.Print("LOSE:"+joined)
        }
    }

    for i, nextWord := range wordList.list {
        copiedDict := dict.copy()
        copiedList := copiedDict[lastChar]
        copiedPath := wordPath.copy()
        copiedPath.add(nextWord)
        copiedList.delete(i)
        copiedDict[lastChar] = copiedList
        DoShiritoriToEnd(copiedDict, nextWord, copiedPath, depth)
    }
}
func main() {
    inFilePath := flag.String("in","","input data file")
    flag.Parse()

    if *inFilePath == "" {
        logger.Print("Input file not spepcified")
        return
    }

    // open input and output files
    logger.Printf("Input file: %s\n", *inFilePath)

    inFile,err := os.Open(*inFilePath, os.O_RDONLY, 0)
    if err != nil {
        logger.Print(err)
        return
    }

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

    // get the first word
    reader := bufio.NewReader(inFile)
    line, err := reader.ReadString('\n')
    if err != nil {
        logger.Print(err)
        return
    }

    firstWord := ExtractFirstAndLastLetter(line[0:len(line)-1])
    startDict := make(WordDictionary, len(CHAR_LIST))
    for {
        line, err = reader.ReadString('\n')
        if err != nil {
            //logger.Print("Error while reading file")
            break
        }

        if len(line) == 1 {
            continue
        }

        word := line[0:len(line)-1]
        word = ExtractFirstAndLastLetter(word)
        //logger.Print(word)
        firstLetter := word[0]
        list := startDict[firstLetter]
        list.add(word)
        startDict[firstLetter] = list
    }

    //logger.Print(startDict)
    var list StringList
    DoShiritoriToEnd(startDict, firstWord, list, 0)
}

入力(level1.txt)

xrdjgorj
jctbqs
jkrshfcilv
jorsezhajk
jmuholsxrc
sfdpdioerpv
silpaavk
vtvauxgju
vogmqa
vnofdnnc
vdruy
uemxza
udbjf
uvtpf
aszhnuvn
awmhkgyd
alhjlmioar
ngzmjyd
nznxgp
dvvcghudkww
dwivxpujzdo
deqwbye
wotaaadgpo
wgaiqosarg
wyeflkkwxg
wzltrkb
oqyjlgapkih
oeqgb
hwfnhx
kcofl
lubhgkf
fywggvxnixm
mxmlqukzcp
pfliwzwot
pzgilzqvwr
teytvanbrsw
tonhvlvg
gnsxomhwdz
zsxyei
ihcsq
ckxuhuty
ydlwmlwskf
rkaqnmnb
btjryytmvye
ezdfvkx
xmzxpvq

実行結果

$ ./shiritori -in=level1.txt
Input file: level1.txt
 WIN:js-sv-vu-ua-an-nd-dw-wo-oh-hx-xq
LOSE:js-sv-vu-ua-an-nd-dw-wo-ob-be-ex-xq
 WIN:js-sv-vu-ua-an-nd-dw-wg-gz-zi-iq
 WIN:js-sv-vu-ua-an-nd-dw-wg-gz-zi-iq
 WIN:js-sv-vu-ua-an-nd-dw-wb-be-ex-xq
LOSE:js-sv-vu-ua-an-nd-do-oh-hx-xq
(後略)

2010年11月12日金曜日

Goの仕様変更

Go一周年ということで久しぶりに最新版にしたところ、コンパイルエラーが発生したのでその原因を探してみた。結果、いくつか仕様に変更があったことが判明。自分に影響があったのは以下の2つ。

  • 2010-10-13のリリースでインターフェースへのポインタが自動的に参照されなくなった
  • 2010-10-20のリリースでlogパッケージのインターフェースが変更になった

インターフェースへのポインタの扱いが変更

リリースノートの該当箇所は以下。

The language change is that uses of pointers to interface values no longer
automatically dereference the pointer.  A pointer to an interface value is more
often a beginner’s bug than correct code.

ようするにインタフェースのポインタとか使うなよ!という変更点。自分のコードでは次のように変える必要があった。ちなみに変更前のコードではconn.Closeでコンパイルエラー(undefinedエラー)が起きてた。

変更前

func communicate(conn *net.Conn) {
    defer func() {
        conn.Close()
    }()
(以下略)

変更後

func communicate(conn net.Conn) {
    defer func() {
        conn.Close()
    }()
(以下略)

logパッケージのインタフェースが変更

個人的に結構影響のあった変更。関数名やらが変わったので色々置換する必要があった。

変更前

var logger *log.Logger = log.New(os.Stdout, nil , "", log.Lok)
logger.Log("Error")

変更後

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