Python を使った gRPC の辛み。2019年11月。(自分用メモ)

gRPC 界では普通なのかもしれませんが、初心者が Python で gRPC クライアントを書こうとして意外とハマったのでメモします。

[Python Quick Start](https://grpc.io/docs/quickstart/python/) を元に作業を行います。まずサンプルコードのダウンロード。ここで、最新の v1.25.0 ではなくてちょっと古い v1.19.0 を使います。理由は後述します。

```
git clone -b v1.19.0 https://github.com/grpc/grpc
```

ドキュメントとは趣向を変えて、別のディレクトリに Python プロジェクトを作ってみます。ここでも v1.19.0 を使います。

```
mkdir python-grpc
cd python-grpc
pipenv --python 3.7
pipenv install grpcio~=1.19.0
pipenv install --dev grpcio-tools
pipenv shell
mkdir pb
```

## 辛み1: grpc_tools.protoc のオプションが難しい。

grpc_tools.protoc コマンドを使って proto ファイルから py を作成して、`pb` ディレクトリに書き込みます。

```shell
python -m grpc_tools.protoc \
    -I../grpc/examples/protos/ \
    --python_out=pb \
    --grpc_python_out=pb \
    ../grpc/examples/protos/helloworld.proto
```

* `-I`: proto 中の include の他、このコマンドで処理する proto ファイルの位置も -I で指定しなければいけない。
* `--python_out`: xxx_pb2.py ファイルを出力するディレクトリを指定します。
* `--grpc_python_out`: xxx_pb2_grpc.py ファイルを出力するディレクトリを指定します。

まず -I オプションは必須です。これが無いと `File does not reside within any path specified using --proto_path (or -I).` というエラーになります。ちゃんと proto の位置を指定してるのになんで探せないのか理不尽です。

また、-I オプションにはコツがあって、例えば `-I../grpc/examples/` のように上位ディレクトリを指定すると `pb` ではなく `pb/protos` にファイルが生成されます。知らないとファイルがどこに行ったのかわからず途方に暮れます。

また、`--help` オプションの解説には `--python_out` と `--grpc_python_out` の違いに触れられていません。上記のように違いはあるのですが、別のディレクトリに入れても import に失敗するだけで良いことないので、わざわざ別のオプションがあるのは理不尽です。

## 辛み2: 生成されたファイルに相対パスが通っていない。

出来たやつを試しに python から読んでみるとエラーが出ます。

```shell
$ python
>>> import pb.helloworld_pb2_grpc
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/tyamamiya/tmp/python-grpc/pb/helloworld_pb2_grpc.py", line 4, in <module>
    import helloworld_pb2 as helloworld__pb2
ModuleNotFoundError: No module named 'helloworld_pb2'
```

なんと生成されたコードの相対パスが間違っている(Python2 風らしいです)。仕方がないので [python: use relative imports in generated modules](https://github.com/protocolbuffers/protobuf/issues/1491#issuecomment-547504972) に従って `pb/__init__.py` を追加します。

```python
import sys
from pathlib import Path

sys.path.append(str(Path(__file__).parent))
```

## コツ: デバッグの仕方

エラーがそっけないのでうまく行かない時は環境変数を設定して実行すると良いです。ここでサーバとクライアントを起動して動作確認します。

```shell
$ python ../grpc/examples/python/helloworld/greeter_server.py & 
$ export GRPC_TRACE=all
$ export GRPC_VERBOSITY=DEBUG
$ python ../grpc/examples/python/helloworld/greeter_client.py
... 大量に色々出る。
Greeter client received: Hello, you!
$ unset GRPC_TRACE
$ unset GRPC_VERBOSITY

```

よしよし。

## 辛み3: google.api

たまに google.api を include している proto ファイルがあります。proto ファイル自体は https://github.com/googleapis/googleapis/tree/master/google/api にあるのでダウンロードして `-I` で指定すればよいのですが、変換後のコードには include した proto の内容は含まれません。`--include_imports` というオプションで取り込んでくれるような気がしたのですがそんな事は無くて、別に変換済のやつをインストールする必要があります。

```shell
pipenv install googleapis-common-protos
```

まあこれはわかっていればどうってこと無いです。

## 辛み4: `missing selected ALPN property.`

最新の Python grpcio ライブラリで 既存の gRPC サーバに TLS で接続すると `missing selected ALPN property.` が出る時があります。

```
D1125 19:20:57.313482000 4619453888 security_handshaker.cc:186]        Security handshake failed: {"created":"@1574677257.313465000","description":"Cannot check peer: missing selected ALPN property.","file":"src/core/lib/security/security_connector/ssl_utils.cc","file_line":118}
I1125 19:20:57.313703000 4619453888 subchannel.cc:1000]                Connect failed: {"created":"@1574677257.313465000","description":"Cannot check peer: missing selected ALPN property.","file":"src/core/lib/security/security_connector/ssl_utils.cc","file_line":118}
```

このキーワードで検索すると色々興味深い事実が見つかるのですが、 https://github.com/grpc/grpc/issues/18710 から察するに、ALPN check という機能に対応していないサーバにアクセス出来ないようです。

仕方がないので古いバージョン v1.19.0 を使ってお茶を濁します。 https://stackoverflow.com/questions/57397723/grpc-client-failing-to-connect-to-server-with-tls-certificates

## 結論

Python と gRPC の組み合わせはこなれてない感がある。