twitchtv/twirp を試した

github.com

gRPCのようなフレームワークで、違いはHTTP 1.1で動くこととJSONをサポートしてること。

インストール

protoc-gen-twirpの他にprotocとprotoc-gen-goも必要。

$ go get github.com/twitchtv/twirp/protoc-gen-twirp

$ brew install protobuf
$ go get github.com/golang/protobuf/protoc-gen-go

protoファイル

まずprotoファイルを書く。

$ mkdir proto
$ vi proto/hello.proto
syntax = "proto3";
package utahta.twirp.example.helloworld;
option go_package = "helloworld";

service HellowWorld {
  rpc Hello(HelloReq) returns (HelloResp);
}

message HelloReq {
  string subject = 1;
}

message HelloResp {
  string test = 1;
}

protocする

protoファイルからgoファイルをつくる。

$ mkdir helloworld
$ protoc --proto_path=./proto --twirp_out=./helloworld --go_out=./helloworld ./proto/hello.proto
$ ls helloworld
hello.pb.go    hello.twirp.go

サーバを書く

$ mkdir server
$ vi server/main.go
package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/utahta/twirp-example/helloworld"
)

type Server struct{}

func (s *Server) Hello(ctx context.Context, req *helloworld.HelloReq) (*helloworld.HelloResp, error) {
    return &helloworld.HelloResp{
        Test: fmt.Sprintf("Subject: %s", req.Subject),
    }, nil
}

func main() {
    s := &Server{}
    handler := helloworld.NewHellowWorldServer(s, nil)
    http.ListenAndServe(":8881", handler)
}

クライアントを書く

$ mkdir client
$ vi client/main.go
package main

import (
    "context"
    "fmt"
    "net/http"

    "github.com/utahta/twirp-example/helloworld"
)

func main() {
    c := helloworld.NewHellowWorldProtobufClient("http://localhost:8881", http.DefaultClient)
    resp, err := c.Hello(context.Background(), &helloworld.HelloReq{Subject: "hello twirp"})
    if err != nil {
        panic(err)
    }
    fmt.Printf("%#v\n", resp)
}

最終的に次のようなディレクトリ構成になった。

.
├── client
│   └── main.go
├── helloworld
│   ├── hello.pb.go
│   └── hello.twirp.go
├── proto
│   └── hello.proto
└── server
    └── main.go

実行する

まずサーバを実行。

$ go run server/main.go

次にクライアントを実行する。 すると結果が返ってくる。

$ go run client/main.go
&helloworld.HelloResp{Test:"Subject: hello twirp"}

curlで実行する

JSONに対応しているのでcurlを使ってさくっとリクエストできる。

$ curl -H 'Content-Type:application/json' -X POST -d '{"subject":"hello curl"}' "http://127.0.0.1:8881/twirp/utahta.twirp.example.helloworld.HellowWorld/Hello"

雑感

シュッと書いたらProtocolBuffersとJSONで会話できるようになってすごい。便利。
学習コストの低さがなによりいい。

2018年にもなって xargs でハマった

BSD と GNU 版の挙動の違いにハマった。

以下、ミニマムに試した結果。

環境
BSD xargs: macOS 10.12.6
GNU xargs: ubuntu 18.04 xargs (GNU findutils) 4.7.0-git

同じ

BSD xargs

$ echo "a b c" | xargs -n1
a
b
c

GNU xargs

$ echo "a b c" | xargs -n1
a
b
c

異なる

BSD xargs

$ echo "a b c" | xargs -n1 -I {} echo {}
a
b
c

GNU xargs

$ echo "a b c" | xargs -n1 -I {} echo {}
a b c

このように異なるおかげで、a, b, c と1つずつ処理しているつもりが、GNU版では一度に処理されてしまって、意図しない動きになっていた。

macOS では動くのに CI で動かない状態だったので、気づくのにすこし時間がかかった。つらい。

man をみたところ、GNU版は、-Iを使うとセパレータが改行になるらしい。ヘー。

面倒くさくなったので、xargs を使わないように改修した。

gcloud コマンドを使って Datastore から BigQuery にシュッとする

特定の Datastore Kind を適当に BigQuery へ持っていきたいとき、gcloud コマンドを使うと便利だった。

gcloud コマンドのインストールはドキュメントのとおり。

Export

エクスポートする App Engine プロジェクトは mcz で、Datastore の Kind は User, UserActionLog とする。

まず GCS(Cloud Storage) にエクスポート先バケットをつくる。
プロジェクトとバケットのリージョンが異なると怒られたので、要注意。

ここでは mcz_export というバケットをつくったことにする。

エクスポートコマンドは次のようになる。

gcloud --project=mcz beta datastore export --kinds="User,UserActionLog" gs://mcz_export/backup20171215

beta コマンドだけどちゃんと動く。

これで mcz_export/backup20171215 に User, UserActionLog のデータがエクスポートされる。

Import

続いて BigQuery にインポートする。

インポート先の dataset は mcz_data とする。

インポートコマンドは次のようになる。

bq load --project_id=mcz --replace --source_format=DATASTORE_BACKUP mcz_data.User gs://mcz_export/backup20171215/all_namespaces/kind_User/all_namespaces_kind_User.export_metadata

--replace はなくても良いが、つけないと BigQuery にテーブルが既に存在するときインポート失敗するようになる。

適当に試したところ、1つずつしかインポートできないようだった。
ググり力が足りないのかもしれない。

ただコマンドを見ればお分かりのとおり、フォーマットが決まっている。

なので、Kind名のところを随時変えてあげれば、まとめてシュッともっていける。

次のようなスクリプトを書くと便利。

#!/bin/bash

BACKUP_NAME="backup"`date '+%Y%m%d'`
KINDS="User,UserActionLog"

gcloud --project=mcz beta datastore export --kinds="${KINDS}" gs://mcz_export/${BACKUP_NAME}

KINDS=`echo $KINDS | tr ',' ' '`
for kind in ${KINDS};
do
  echo "${kind} importing..."
  bq load --project_id=mcz --replace --source_format=DATASTORE_BACKUP mcz_data.${kind} gs://mcz_export/${BACKUP_NAME}/all_namespaces/kind_${kind}/all_namespaces_kind_${kind}.export_metadata
done

もっとしっかりやるなら Cloud Dataflow というものがあるのでそれを使うと良いらしい。