Protocol BuffersをJSONに変換する

React Native + GoのProobuf API Serverの構成でアプリケーションを作っているのですが、次の条件でdeserializeができないことがわかってしまった。

decodeIO/protobuf.js の方も一応試してみたけど、うまく動かないので急遽Protobufの使用を諦めてjsonに切り替えることにします。 こちらも詳細は↓にまとめました。あとでGithubの方にもIssue建てようかと思います

protobuf/jsonpbJSONに変換する

開発期間も短いので今からサーバの実装を変えていくのはできればさけたい。 モックサーバとかはすでにProtobufで書いていたので、それをJSONに変換する処理を書いていく方法を検討してみた。

今回使っている型はそんなに多くないので自前でさっと書けないかなとか考えつつ調べてみたら、protobufの中に jsonpb という便利そうなモジュールがあった。 ProtobufのモックAPIJSONAPIに書き換えてみる。

func confirmProfile(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)

    p := &pb.ConfirmProfileResponse{
        HasProfile: true,
        Profile: &pb.ProfileResponse{
            Uid: "uid0001",
            Username: "c_bata_",
            IconUrl: "http://example.com/hoge.png",
            WebsiteUrl: "http://example.com",
            CreatedAt: 1000,
            UpdatedAt: 1000,
        },
    }

    m := jsonpb.Marshaler{
        EmitDefaults: true,
        Indent: "    ",
        OrigName: true,
    }
    m.Marshal(w, p)
    return
}

curlで確認。

$ curl localhost:8080 
{
    "has_profile": true,
    "profile": {
        "uid": "uid0001",
        "username": "c_bata_",
        "icon_url": "http://example.com/hoge.png",
        "website_url": "http://example.com",
        "created_at": "1000",
        "updated_at": "1000"
    }
}

実装はreflectを多用することになるので、パフォーマンスには課題があるかもしれませんが簡単にJSONAPIに変換できた。 ただ、数値(int64)で欲しかったものがstringになってしまっていて、整数値として出力してほしい。 もしかするとバグかなと思い、ResponseWriterの代わりに次のLoggerBufferを仕込みつつ原因となっている行を探してみた。

type LoggerBuffer struct {
    bytes.Buffer
}

func (b *LoggerBuffer) Write(p []byte) (n int, err error) {
    log.Printf("LoggerBuffer: %s\n", p)
    return b.Buffer.Write(p)
}

次の行が原因らしく、どうやらint64とuint64の型の場合はあえて、ダブルクオーテーションで囲っている。 こうしている理由は、JavaScriptの仕様に何か関係しているのかもしれないと思い調べてみた。

https://github.com/golang/protobuf/blob/master/jsonpb/jsonpb.go#L504-L511

EcmaScriptのNumber型

調べてみると次のStackoverflowの回答が見つかった。

math - What is JavaScript's highest integer value that a Number can go to without losing precision? - Stack Overflow

Number型で表現可能なのは +/- 253 の範囲らしい。 あとbit演算は32bitの範囲で利用できる。 実際にChromeのConsoleで挙動をみてみた。

> Math.pow(2, 53)
9007199254740992
> Math.pow(2, 64)
18446744073709552000
> Math.pow(2, 53) - 1
9007199254740991
> Math.pow(2, 53) + 1
9007199254740992

値としては持つことができているが、253を超える範囲では数値演算が正しく動作しないことが確認できる。 負の数も同様。

> - Math.pow(2, 53)
-9007199254740992
> - Math.pow(2, 53) -1
-9007199254740992
> - Math.pow(2, 53) + 1
-9007199254740991

そもそも何故 int64を使ったのか

GoのAPIサーバとJS間で、文字列よりも言語間で統一されてそうというイメージで時刻はUnixTimeで表現していた。 golangでは time.Time 型に Unix() メソッドが提供されている。 このメソッドは int64 を返していたので、その値をProtocol Buffersでも使用していた。

https://godoc.org/time#Time.Unix

ただし、先程のEcmaScriptのNumber型の仕様上、JSでint64を扱うのはやめておいたほうがよさそう。 関連する問題に、「2038年問題」というのがあるみたい。

2038年問題 - Wikipedia

こういった問題がある以上、API上での時間の扱いとしてUnixTimeを使うのはあまりいい方法ように思えてきた。

ISO8601とRFC3339

ISO8601形式がよさそう。文字列なので先程のような問題は解決できる。 今後はこれを使おうかと思う。

golangtime.Time をISO8601形式に変換する

time.Now().UTC().Format(time.RFC3339)

あとは、go-sql-driver/mysqlのlocの設定もつけておいた ( ?parseTime=true&loc=Local )。

おわりに

常識だったのかもしれない。この機会に知ることができてよかった。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

asyncioがPOSIXスレッドを使っている原因を調べる

tokibito先生 (id:nullpobug) がオフィスに遊びにおいでと声かけてくれたので、オープンコレクターさんに遊びに行ってました。 aodag先生 (id:aodag) と3人で雑談してたんですが、ふと以前気になっていたことを思い出したので聞いてみた。

気になっていたこと

とある勉強会の発表資料 を作っている時に、 asyncioとaiohttpを使ってとあるサーバにHTTPのリクエストを送るコード例を用意した。

import aiohttp
import asyncio

async def fetch(l, url):
    async with aiohttp.ClientSession(loop=l) as session:
        async with session.get(url) as response:
            return await response.text()


async def main(l, url, num):
    tasks = [fetch(l, url) for _ in range(num)]
    return await asyncio.gather(*tasks)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(main(loop, 'http://localhost:8000', 3))
    for r in results:
        print(r)

PyCharmでは、「Concurrency Diagram」を表示する機能があり、スレッドやプロセスの動きを確認できるのですが、 このコードを実行したときのスレッドの動きは次のようになる。

f:id:nwpct1:20170330234242p:plain

なぜか、 concurrent.futures.ThreadPoolExecutor が現れている。 ドキュメントに何か書いてあるのか調べてみたのですが、それらしい記述が見つからず諦めていたのでaodag先生とtokibito先生に聞いてみた。

socket.getaddrinfo

数十分で原因を見つけてくれた。 socket.getaddrinfo が同期的に実行されてしまうため、cpythonの実装としてはこれを非同期に実行できるように変えるのではなく、ひとまず concurrent.futures.ThreadPoolExecutor により複数のスレッドで実行するようにしているらしい。

試しにgetaddrinfoが使われないコードサンプルとして、 aioredisを使ったサンプルを用意した。 ローカルに建てたRedisのサーバにUNIXドメインソケットで繋いでみる。

import asyncio
import aioredis

async def connection_example(key):
    conn = await aioredis.create_connection('/tmp/redis.sock')
    return await conn.execute('GET', key)


async def main(num):
    tasks = [connection_example('my-key') for _ in range(num)]
    return await asyncio.gather(*tasks)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(main(3))
    for r in results:
        print(r)

ちなみにredisのconfigは↓。

daemonize no
pidfile /var/run/redis.pid
unixsocket /tmp/redis.sock
unixsocketperm 700
logfile ""
databases 1

この時のConcurrency Diagramを見ると、

f:id:nwpct1:20170331163806p:plain

たしかにスレッドが生成されていない。

外部のRedisサーバへのアクセス

一方でRedisのサーバを外部に用意して繋いでみると (今回は arukas.io を使わせていただきました)、

import asyncio
import aioredis

async def connection_example(key):
    conn = await aioredis.create_connection(
        ('seaof-xxx-xxx.arukascloud.io', 311390),
        db=0, password='xxxxxxxxxxxxxxx')
    return await conn.execute('GET', key)


async def main(num):
    tasks = [connection_example('my-key') for _ in range(num)]
    return await asyncio.gather(*tasks)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    results = loop.run_until_complete(main(3))
    for r in results:
        print(r)

実行すると次の通り。

f:id:nwpct1:20170331163620p:plain

おー やっぱりワーカースレッドが生成されてしまうらしい。

ThreadPoolExecutorのワーカースレッドはいくつまで生成されるのか。

Semaphoreで同時に実行される数を3つに制限した時のConcurrency Diagramは次のようになる。

f:id:nwpct1:20170331164208p:plain

どうやらThreadPoolExecutor内のThreadが再利用されていない。 どこまで生成されるのかは、ドキュメントのThreadPoolExecutorのところに書いてあった。

max_workers が None か指定されない場合のデフォルト値はマシンのプロセッサの数に 5 を掛けたものになります

17.4. concurrent.futures – 並列タスク実行 — Python 3.6.1 ドキュメント

試しに30個ぐらいリクエストを送ってみる(Semaphoreは3)。

f:id:nwpct1:20170331164432p:plain

20個まで生成され、それ以降は再利用されているのを確認できた。 実装上ワーカースレッドの上限は変更できないけど、たしかに別に困るケースもなさそう。

uvloop

uvloopの方は、POSIXスレッド使わずに頑張ってるかもという話が出たので確認。

import uvloop

# 中略
loop = uvloop.new_event_loop()
asyncio.set_event_loop(loop)

実行すると、

f:id:nwpct1:20170330232815p:plain

おー ほんとだ。

おわりに

aodag先生とtokibito先生すごい… 自分はドキュメントを読みつつモヤモヤしたまま放置してたのですが、数十分で原因を見つけて教えてくれた。 自分もいよいよ明日から社会人なので、お二人目指して精進します。

aodag先生とtokibito先生のいるオープンコレクターさん、お仕事募集中だそうです(お世話になったので一言宣伝)。 ありがとうございました!

2017年抱負(エンジニアリング)

今週のお題「2017年にやりたいこと」

あけましておめでとうございます。 今年やりたいことを書き出してみる。

nwpct1.hatenablog.com

2016年はインプットのかなり多い年だったので、今年はアウトプットを増やしたい。

  • 卒論の提出と卒業: まずはちゃんと卒業します。
  • 書籍の出版: 実はもう動いているので必ずやりきる。これは2016年の目標でもありましたが、告知できるのは今年の5月頃になりそうです。
  • Webサービスをリリース: いま作りたいものがあるのでなんとか形にしたい。
  • Github 100 stars: 2016年の目標だったんですが、最も集めたのはKobinの50starsでした。今年こそ。

うーん... 直近でやりたいこと・やらないといけないことが多くてあんまり思いつかなかった。 もうちょっと余裕を持った生活が一番必要かもしれない。

抱負とまではなっていないんですが、今気になってる技術は、

  • PostgreSQLドライバ実装: 以前、nakagamiさんにPostgreSQLのMessage Formatプロトコル を丁寧に解説してもらったので、一度書いておきたい
  • Rustでシステムコールを直接バンバン叩く何かつくってみたい: 作るもの思いつかなければ、HTTPのサーバとかいいかな...
  • 非同期のPythonサーバの実装: まだServerとのInterfaceがPEPに無いので、何らかの形で関われると嬉しい

おわりに

今年もよろしくお願いします。