Golden Road

信じた道なら行けばいい

gRPC(Go)でタイムアウト時にio.EOFエラーになる件を調査した

WEB+DB PRESS Vol.110のgRPC特集がおもしろくて手を動かしながら読んでいる。

WEB+DB PRESS Vol.110

WEB+DB PRESS Vol.110

タイムアウト設定の事も書かれていて、そこに

設定した期限を過ぎた場合、クライアントスタブは応答を待ち受けるのをやめてステータスコードDeadlineExceeded(4)として処理を終了します。

とあったので試したところ、想定と異なった挙動になったのでメモしておく。

現象

一部省略するが、メインとなるコードは下記である。

    c := pb.NewFileServiceClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    stream, err := c.Upload(ctx)
    if err != nil {
        log.Fatalf("Could not upload file: %v.", err)
    }

    buf := make([]byte, 1000*1024)
    for {
        n, err := fs.Read(buf)
        if err == io.EOF {
            break
        } else if err != nil {
            log.Fatalf("Could not read file: %v.", err)
        }

        if err := stream.Send(&pb.FileRequest{Name: filepath.Base(name), Data: buf[:n]}); err != nil {
            log.Fatalf("Could not send file: %v.", err)
        }
    }

    res, err := stream.CloseAndRecv()
    if err != nil {
        log.Fatalf("Could not receive response file: %v.", err)
    }

この時、私は stream.Send の部分で DeadlineExceeded エラーが返ると期待していたが実際は io.EOF エラーだった。

調査

気になったのでgrpc-goのコードを辿って調査した。
長くなるので重要だと思った箇所を記載する。

また、私の解釈としてStreaming RPCには接続保持側とストリーミング通信側があると解釈しているので2つに分けて記載する。
例えば上記コードで言うと Upload() が接続保持側で Send() がストリーミング通信側である。

接続保持側

Contextのハンドリングをしているのはこちら側

ここまでで接続保持側の終了処理が完了。

ストリーミング通信側

上記のように DeadlineExceeded ではないエラーが返り終了する。
ストリーミング通信側はContextを見るのではなく、接続保持側からの操作によって終了する。

解決策

Send でエラーが返ってきた場合、 Fatal 等で終了させずに break させて CloseAndRecv() まで実行してエラーハンドリングを行うと良い。
Send のエラーは警告くらいの気持ちで Print だけしておけば良いと思う。

   for {
        n, err := fs.Read(buf)
        if err == io.EOF {
            break
        } else if err != nil {
            log.Fatalf("Could not read file: %v.", err)
        }

        if err := stream.Send(&pb.FileRequest{Name: filepath.Base(name), Data: buf[:n]}); err != nil {
            log.Printf("Could not send file: %v.", err)
            break
        }
    }

    res, err := stream.CloseAndRecv()
    if err != nil {
        log.Fatalf("Could not receive response file: %v.", err)
    }

変更箇所は2行。

       if err := stream.Send(&pb.FileRequest{Name: filepath.Base(name), Data: buf[:n]}); err != nil {
-          log.Fatalf("Could not send file: %v.", err)
+           log.Printf("Could not send file: %v.", err)
+           break
        }

するとこんな感じでログが出る。

2019/04/29 08:18:57 Could not send file: EOF.
2019/04/29 08:18:57 Could not receive response file: rpc error: code = DeadlineExceeded desc = context deadline exceeded.
exit status 1

これでタイムアウトのときに DeadlineExceeded が返るようになった。

まとめ

  • タイムアウトだった場合でも Send() のエラーは io.EOF が返る。
  • Send() でエラーが出てもそこで終了せずに CloseAndRecv() まで実行してエラーを確認。
  • タイムアウトの場合は CloseAndRecv() のエラーで DeadlineExceeded エラーが取れる。

私はgRPCを仕事でも使ったのだが、protoファイルを書くだけでサーバのインタフェースとクライアントのAPIコール関数を生成してくれる。
サーバはインタフェースの実装を書くだけだし、クライアントは関数を呼び出す感覚でAPIコールができる。
これはすごく楽なのではないだろうか。

特にマイクロサービスアーキテクチャを採用している場合にはおすすめです!

スプレッドシートのデータを取得する

slack botスプレッドシートからデータ取得する場面があったので調査して検証がてらgolangで実装してみた。
気をつける所とかメモ的に書いておく。

作ったのはスプレッドシートで重み付け抽選を行うアプリ。

github.com

認証

認証方法はAPIキー、OAuth、サービスアカウントの3つがある。

f:id:i178inaba:20190313020229p:plain

いずれもGCPの『APIとサービス』から取得できる。

APIキー

公開されたスプレッドシートしか読み込めないようだったので今回はパス。

OAuthクライアントID

一度ユーザがブラウザを開いて認証する必要がある。
bot用には使えないなーと思ったのでパス。

サービスアカウント

新しくGoogleアカウントを発行するイメージ。
スプレッドシートに招待して使う。

招待してしまえばずっと使えるためbotには合っていると思い、今回はサービスアカウントを使用することにした。

使用方法

発行したjsonキーファイルの中から client_email を取り出す。

$ cat /path/to/key.json | jq -r .client_email
test@quickstart-1551059800000.iam.gserviceaccount.com

データを取得したいスプレッドシートに移動して右上の共有ボタンをクリック。
『他のユーザと共有』ダイアログを開き、先程取得した client_email を追加する。

f:id:i178inaba:20190313022722p:plain

実装

実装側の認証には google.JWTConfigFromJSON を使う。
キーjsonファイルの中身とスコープ(今回は取得なのでreadonly)を渡す。

conf, err := google.JWTConfigFromJSON(
    jsonKeyBytes, 
    "https://www.googleapis.com/auth/spreadsheets.readonly"
)

返ってきた conf からhttpクライアントを取得し、 sheets.New に渡して Service を取得する。

srv, err := sheets.New(conf.Client(ctx))

あとは取得したサービスを使ってGetすればいい。
スプレッドシートIDと取得するデータのレンジ(下記の例では A2:B )を指定する。
( A2:B はA2セルからB列の最後のセルまでという意味)

resp, err := srv.Spreadsheets.Values.Get(spreadsheetID, "A2:B").Context(ctx).Do()

var items []lotteryItem
for _, row := range resp.Values {
    name := row[0].(string)
    weight, err := strconv.Atoi(row[1].(string))

    items = append(items, lotteryItem{name: name, weight: weight})
}

※エラーハンドリングは省略してあります。

列方向への取得

デフォルトでは行方向の取得になるが、列方向に取得したい場合は MajorDimensionCOLUMNS を指定すればよい。

resp, err := srv.Spreadsheets.Values.Get(spreadsheetID, "A2:B").Context(ctx).MajorDimension("COLUMNS").Do()

参考

developers.google.com godoc.org

Nuxt.js×Express使用時のhost指定でハマった

Nuxt.js×ExpressのDockerfileを書いている時に、コンテナ外からアクセスを受け付けようと思って server/index.jshost の値を 127.0.0.10.0.0.0 に書き換えた。

host = process.env.HOST || '0.0.0.0',

しかしコンテナ外からアクセスできなかった。

よく見ると nuxt.options.server で上書きされているようだった。

const {
  host = process.env.HOST || '0.0.0.0',
  port = process.env.PORT || 3000
} = nuxt.options.server

(最近のjsに慣れてなくて、この記法の意味調べるのに手間取った...)

nuxt.options.server のデフォルト値

github.com

環境変数 HOST0.0.0.0 を指定するとコンテナ外からのアクセスは通った。

まとめ

ENV HOST 0.0.0.0

追記

結局上書きされるなら server/index.js で代入してる所無駄じゃない?と思ってPR出してみた。

github.com

追記2

PRマージされてた😊

LinuxにNode.js入れる時はPermissionのためにnvm使ったほうがいい

前回の記事ではyumでNode.jsをインストールしたが、それはServerlessをインストールしようと思ったためであった。

しかし、Node.jsのインストール後、Serverless公式に書いてあったコマンド
npm install serverless -g
を実行したらPermissionで怒られた。

npm ERR! Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
npm ERR!  { Error: EACCES: permission denied, access '/usr/local/lib/node_modules'
npm ERR!   errno: -13,
npm ERR!   code: 'EACCES',
npm ERR!   syscall: 'access',
npm ERR!   path: '/usr/local/lib/node_modules' }
npm ERR!
npm ERR! Please try running this command again as root/Administrator.

Permissionで怒られたのでsudoつけて実行したら以下のようなエラーが出た。

┌───────────────────────────────────────────────────┐
│          serverless update check failed           │
│        Try running with sudo or get access        │
│       to the local update config store via        │
│ sudo chown -R $USER:$(id -gn $USER) /root/.config │
└───────────────────────────────────────────────────┘

インストールはできていてコマンドは使えるようだったが、このようなエラーを放置するのはちょっと気持ち悪いのでnpmのPermission周りについて色々調べた。

すると以下のページを発見。

Resolving EACCES permissions errors when installing packages globally

ここの『Reinstall npm with a node version manager』に以下の記述がある。

This is the best way to avoid permissions issues.

node version managerでnpmをインストールする事がPermission問題を回避する最善の方法らしい。
そのままリンクを辿っていくとversion managerの紹介がされていた。

Using a Node version manager to install Node.js and npm

ここでnとnvmが紹介されていたのだが、nvmの方がスター数が多かったため今回はnvmを選択した。

nvmのインストール方法は割愛する。
以下nvmレポジトリの Installation を参照してください。

github.com

nvmのインストール後、 nvm install node で最新のNode.jsがインストールされる。
その後Serverlessをインストールするとエラーは出なかった🎉

まとめ

LinuxにNode.js入れる時はPermissionのためにnvm使ったほうがいい!

Amazon Linuxで古いNode.jsがインストールされる時の解決方法

Amazon Linuxで何度Node.js v11をインストールしようとしてもv6が入ってしまう現象が起こったのでその解決策をメモ。

現象

v11用RPMのセットアップをする。

$ curl -sL https://rpm.nodesource.com/setup_11.x | sudo bash -

その後、yum installしても 2:6.14.4-1nodesource がインストールされる。

$ sudo yum install -y nodejs
...
Installed:
  nodejs.x86_64 2:6.14.4-1nodesource

解決策

以下のコマンドでRPMのキャッシュを消す。

$ sudo yum remove -y nodesource-release* nodejs
$ yum clean all
$ sudo rm -rf /var/cache/yum/*
$ sudo rm /etc/yum.repos.d/nodesource-el.repo

その後、再度RPMセットアップコマンドを流してyum installすればよい。

$ curl -sL https://rpm.nodesource.com/setup_11.x | sudo bash -
...
$ sudo yum install -y nodejs
...
Installed:
  nodejs.x86_64 2:11.1.0-1nodesource

v11が入りました。

参考

github.com