きみはねこみたいなにゃんにゃんなまほう

ねこもスクリプトをかくなり

CSS の position: sticky を試してみる

Material UI の ListSubHeader の sticky な振る舞いって、どう実装されているんだろうと覗いてみたら position: sticky とだけ書かれていて「こんなプロパティをブラウザがサポートする時代になったんだな」ということを今更知ったので試してみます。

本文中は実際のコードの要点だけを紹介していますので、完全版のコードは以下のリポジトリをご覧ください。

今回は sticky なヘッダとフッタを持った要素が大量に並んでいるというデモを作成しました。

f:id:lightbulbcat:20190405020639g:plain

やったととしては、こんな感じの要素を大量に用意して

<div class="item">
  <header>header</header>
  <div class="content">
    content
  </div>
  <footer>footer</footer>    
</div>

こんな感じの CSS (装飾用途のプロパティは省いています)を書くと

    .item header {
      position: sticky;
      top: 0;
    }
    .item footer {
      position: sticky;
      bottom: 0;
    }

上で挙げたような sticky なヘッダーやフッターが実に簡単に実装できます。

楽すぎますね。

現時点で対応ブラウザは FirefoxChrome、それに -webkit-stickySafari も対応しています(macOS)。

一般的になった UI 要素をブラウザがネイティブサポートするこの感じ、 box-shadowborder-radius が実装された頃を思い出しますね。ちょっとそれっぽいデザインにしようとすると、すぐにWebページが画像の集合体になってしまっていたあの頃に比べたら、今のウェブ開発は異次元と言っていいほどの至れり尽くせり具合ですね。

dataloader を使ってリクエストを取りまとめる React デモを作る

以前 GraphQL を利用していたときに、サーバサイドのSQLリクエストを減らすために使おうとしていたパッケージに dataloader というものがあります。

現在は GraphQL ではなく gRPC-Web を利用したアプリを書いているのですが、dataloader は別に GraphQL に限ったものではなく、id 単体を指定するような複数のリクエストを、複数の id を指定する単一のリクエストにまとめて実行するということをやってくれます*1。つまり dataloader

get****ById(id1) x n → get****ByIds(id1, id2, ...)

という処理を行ってくれる中間層として機能してくれるものになります。

デフォルトではレスポンスに含まれる id を利用して、リクエストの取りまとめや重複するキーへのリクエストの dedupe 処理を行ってくれます。 GraphQL のサーバサイドではスキーマの patching を行っていたのですが、GraphQL はレスポンスに id を含めるかどうかが叩く側次第なためこの辺りの処理ががうまくいかずに頓挫しました。

話を戻して、今回は GraphQL ではなく、一般化したリモートリクエストをWebから行うようなAPIクライアントを対象にして、dataloader の動作を以下のような単純なアプリを作って試していきたいと思います。

f:id:lightbulbcat:20190322010347p:plain

動作の詳しい仕組みは理解していませんが、とりあえず使ってみるところから始めます。

この記事で説明しているコードの、実際に動作するプロジェクトは以下の til リポジトリに置いてあります。

下準備

API クライアントのモックを定義する

今回デモで利用するAPIは以下のようなものです。

// api.ts

// Data Types
// ----------

export type IdType = number

export interface User {
  id: IdType
  name: string
  favoriteThingIds: IdType[]
}

export interface Thing {
  id: IdType
  name: string
}

// Data Mock
// ---------

const thingList: Thing[] = [
  { id: 1, name: "Raindrops" },
  { id: 2, name: "Kittens" },
  { id: 3, name: "Kettles" },
  { id: 4, name: "Mittens" },
  { id: 5, name: "Packages" }
]

const userList: User[] = [
  { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] },
  { id: 2, name: "Bob", favoriteThingIds: [2, 4] },
  { id: 3, name: "Carol", favoriteThingIds: [1, 3] }
]

// API Mock
// --------

const emulateNetworkDelay = <V>(value: V): Promise<V> => {
  return new Promise(resolve => setTimeout(() => resolve(value), 1000))
}

export default {
  getUserById(id: IdType) {
    console.log(`API called: getUserById(${id})`)
    const user = userList.find(u => u.id === id)
    return emulateNetworkDelay(user)
  },
  getUsersByIds(ids: IdType[]) {
    console.log(`API called: getUsersByIds(${ids})`)
    const users = ids.map(id => userList.find(u => u.id === id))
    return emulateNetworkDelay(users)
  },
  getThingById(id: IdType) {
    console.log(`API called: getThingById(${id})`)
    const thing = thingList.find(t => t.id === id)
    return emulateNetworkDelay(thing)
  },
  getThingsByIds(ids: IdType[]) {
    console.log(`API called: getThingsByIds(${ids})`)
    const things = ids.map(id => thingList.find(t => t.id === id))
    return emulateNetworkDelay(things)
  }
}

データ型としては UserThing への参照を id のみで持っており、Thing の内容を見たい場合は更にAPIを叩く必要がある、というAPI設計です。

ByIds のクエリにより id 複数指定でデータを取得することができるという想定です。dataloader の利用にはこういったAPIが存在することが前提となります。

しかしこの ByIds 系のクエリですが、実際に React アプリを書いた経験から言うと、意外と活かしづらいです。私が React を使う場合、コンポーネントがマウントされたタイミングでそのコンポーネントが必要とするリクエストを行う、という方針でコンポーネントを切り分け・実装することが多いです。なのでリクエストの効率化のために ByIds でデータ取得の取りまとめを行おうとすると、ちょっと面倒くさく感じる場面が多いですね。

この辺の処理を React のデータフローだけで組もうとすると、実際にデータが必要なコンポーネントのより親階層の方で子コンポーネントに必要なリクエストを取りまとめて行う必要がありそうですが、それをやろうとすると描画コンポーネントと、データを取得するコンテナコンポーネントの間の仕様的な結合が強くなり、使い回しをしづらいコンポーネントになってしまうのではないかと思っています。

という事情もあり ByIds 的なAPI側が用意されているにも関わらず、実際には React での組みやすさによる制約が働いて「複数 id 指定使ってないです、すみません...」となることが今までよくありました。

APIクライアントに対して dataloader を噛ませたラッパーを用意する

さて、前置きが長くなりましたが、上記のAPIクライアントに対して、以下のような dataloader を定義してAPIクライアントのラッパーを定義します。

// api-with-dataloader.ts

import DataLoader from "dataloader"
import api, { IdType } from "./api"

// DataLoaders
// -----------

const userLoader = new DataLoader((keys: IdType[]) => api.getUsersByIds(keys))
const thingLoader = new DataLoader((keys: IdType[]) => api.getThingsByIds(keys))

// Batching API Mock
// -----------------

export default {
  getUserById(id: IdType) {
    return userLoader.load(id)
  },
  getThingById(id: IdType) {
    return thingLoader.load(id)
  }
}

UserThing のそれぞれに dataloader を仕込んだAPIクライアントのラッパーを定義します。ByIds 分の定義も loadMany を利用すればできそうですが、今回のデモでは利用しないので省略します。

React コンポーネントを定義する

これらのAPIクライアントを利用するコンポーネントを以下のように組みます。

import React, { Component, useEffect, useState } from "react"
import logo from "./logo.svg"
import "./App.css"

import api, { IdType, User, Thing } from "./api"
import batchApi from "./api-with-dataloader"

// ThingSummary
// ------------

interface ThingSummaryProps {
  id: IdType
  batch?: boolean
}

function ThingSummary(props: ThingSummaryProps) {
  const { id, batch } = props
  const [thing, setThing] = useState<Thing | undefined>(undefined)

  useEffect(() => {
    if (batch) {
      batchApi.getThingById(id).then(setThing)
    } else {
      api.getThingById(id).then(setThing)
    }
  }, [])

  if (!thing) return <div>loading...</div>

  return (
    <div>
      #{thing.id}: {thing.name}
      {batch && " (batched!)"}
    </div>
  )
}

// UserSummary
// -----------

interface UserSummaryProps {
  id: IdType
  batch?: boolean
}

function UserSummary(props: UserSummaryProps) {
  const { id, batch } = props
  const [user, setUser] = useState<User | undefined>(undefined)

  useEffect(() => {
    if (batch) {
      batchApi.getUserById(id).then(setUser)
    } else {
      api.getUserById(id).then(setUser)
    }
  }, [])

  if (!user) return <div>loading...</div>

  return (
    <div>
      <h3>
        #{user.id}: {user.name}'s Favorite Things
        {batch && " (batched!)"}
      </h3>
      <ul>
        {user.favoriteThingIds.map(id => (
          <ThingSummary key={id} id={id} batch={batch} />
        ))}
      </ul>
    </div>
  )
}

// Main Component
// --------------

export default class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <div>
            <UserSummary id={1} batch />
            <UserSummary id={2} batch />
            <UserSummary id={3} />
          </div>
        </header>
      </div>
    )
  }
}

UserThing それぞれに Summary コンポーネントを定義して、それぞれのコンポーネントは必要なデータを自身がマウントされた際にリクエストを飛ばして取得する、という設計です。こういう処理が簡単に書けるので React Hook 便利ですね。

それぞれの Summary コンポーネントには batch オプションをプロパティとして定義しています。有効化された場合には素の api の代わりに dataloader を仕込んだ batchApi が利用されます。batch が指定された UserSummary は自身のリクエストの他に子コンポーネントThingSummarybatch オプションも有効化するようにしています。

全体としては、id12 のユーザ対しては batch リクエストとして dataloader を利用したAPIクライアントの方を利用し、3 のユーザに対しては元のAPIクライアントを利用する、という描画内容になっています。

実行する

実行結果をイメージするために、改めてデータの中身と描画部分のコードを載せておきます。

const thingList: Thing[] = [
  { id: 1, name: "Raindrops" },
  { id: 2, name: "Kittens" },
  { id: 3, name: "Kettles" },
  { id: 4, name: "Mittens" },
  { id: 5, name: "Packages" }
]

const userList: User[] = [
  { id: 1, name: "Alice", favoriteThingIds: [1, 2, 3, 4, 5] },
  { id: 2, name: "Bob", favoriteThingIds: [2, 4] },
  { id: 3, name: "Carol", favoriteThingIds: [1, 3] }
]
            <UserSummary id={1} batch />
            <UserSummary id={2} batch />
            <UserSummary id={3} />

もしこれらのデータが全て ById によって取得された場合、叩かれるAPIの回数としては以下のようになりますね。

  • getUserById: 3 リクエス
  • getThingById: 9 リクエス

実際にアプリを実行してみると冒頭に貼った画像のように、以下のように描画されます。 「batched!」と描画されている箇所が batch オプションを指定した箇所です。 問題なくデータは指定した id の通りに取得されているようですね。

コンソールに出力されているのは dataloader をかませていない、素のAPIクライアントの各メソッドが呼ばれたログです。

f:id:lightbulbcat:20190322010347p:plain

コンソールの行数だけ見ても、明らかにリクエスト数は減っていますね。実際、APIクライアントが叩かれた回数としては

  • getUserById: 1 リクエスト( id: 3)
  • getUsersByIds: 1 リクエスト(id: 1, 2)
    • 直接コンポーネントからは呼んでいませんが dataloader 経由で呼ばれていますね
  • getThingById: 2 リクエスト(id: 1, 3)
  • getThingsByIds: 1 リクエスト(id: 1, 2, 3, 4, 5)
    • こちらも同様です

となりました。確かに ById リクエストが ByIds リクエストにまとめられていることがわかります。 実際に動作することがつかめたので、使い慣れたあたりで、次は dataloader の動作原理にも踏み込んでみたいですね。

今回は以上です。

*1:まとめられる単位はソースを軽く眺めたところ実行コンテキスト単位(?)な雰囲気を感じましたが、詳しくはまだ理解できていません

VSCode で調べたショートカットリスト

目的は備忘録兼、後で見返して悦に入りたいからです。

環境は MacOS です。

すでに使っていたショートカット

思い出したら書いていきます。

新しく調べて覚えたくなったショートカット

  • Command + Enter: カーソル行の後に空白行を追加
    • Vimo 相当の動作が恋しくなったので調べました
    • 行末移動 + Enter の手間が削減できました

protoc-gen-xxxx のオプションを確認する方法を探す

protoc を使いサーバ/クライアント実装の組み合わせや変換プロキシなどのエコシステムを色々と検証しているのですが、そのせいもあって手元にある分だけでも 7 つ程の protoc-gen-xxxx プラグインがインストールされています。

$ protoc-gen-
protoc-gen-dart          protoc-gen-grpc-gateway  protoc-gen-ts
protoc-gen-go            protoc-gen-grpc-web      
protoc-gen-govalidators  protoc-gen-swagger

これらがあることで

protoc -I. \
  --grpc-gateway_out=logtostderr=true:. \
  my.proto

のようにデフォルトではサポートされていない --xxxx_out オプションを使いコードを生成することができるのですが、そのオプションで実行される protoc-gen-xxxx の更にそのオプションである --xxxx_out="<plugin-options>:out" の種類がプラグインごとに様々だったり、確認方法が提供されているかもまちまちだったりするのでちょっと整理してみたいです。

protoc-gen-xxxx はコマンドとして実行できる

まず基本的なところの確認です。

protoc-gen-xxxxprotoc で使うためにはパスを通してコマンドとして実行できるようにしておく必要があります。 なのでそのプラグインが使えている状態であれば、protoc-gen-xxxx は単体のコマンドとしても実行できます。

標準入力を受け取り動作するようなので適当に dummy-contents と入力して Ctrl+d で EOF を送ると「proto として読めないよ」という旨のようなエラーが返されます。

$ protoc-gen-grpc-gateway
dummy-contents
F0922 13:33:41.840980   97540 main.go:45] failed to unmarshal code generator request: proto: can't skip unknown wire type 4

という、基本的なインタフェースとしては protocprotoc-gen-xxxx は標準入出力を介して繋がっているだけなので、割とシンプルで分かりやすいです。

コマンドライン引数でオプションを渡してヘルプを見る

--xxxx_out="<plugin-options>:out" として protoc に渡しているオプションはコマンドラインからプラグインを実行する際にはコマンドライン引数として渡すことができるようです。

protoc-gen-grpc-gateway で試してみると以下のようにヘルプが表示されます。

$ protoc-gen-grpc-gateway -h
Usage of protoc-gen-grpc-gateway:
  -allow_delete_body
        unless set, HTTP DELETE methods may not have a body
  -allow_repeated_fields_in_body body
        allows to use repeated field in body and `response_body` field of `google.api.http` annotation option
:  -logtostderr
        log to standard error instead of files
:
  -v value
        log level for V logs

この -h でヘルプが見られるのは protoc-gen-grpc-gatewayコマンドラインオプションの実装に flag というパッケージを使っているからです。

ついでに golang/glog というロギングパッケージのオプションも同様に flag で実装され、そのオプションも protoc-gen-grpc-gateway のオプションとして露出しているようなので、ログを標準入力に出す -logtostderr や粒度を設定する -v (verbose) オプションも利用できます。こういうオプションが備わっているプラグインだとデバッグなんかも楽になりますね。

と言ってもこの方法が使えるのは手元のプラグインの中で言えば protoc-gen-grpc-gateway 系だけみたいです。

--grpc-gateway_out=logtostderr=true,v=1:out

のように verbose レベルを指定できます。

諦めてコードを読む

オプションにヘルプが用意されているプラグインはいいですが、大部分のプラグインには用意されていないようなのでその場合はドキュメンテーション頼りか、コードを読んで対応する場合が多いです。自分が知らないだけでプラグインのオプションを確認する方法があるのかもしれませんが...

大抵 GitHubリポジトリoption や存在することが分かっているオプション名などのキーワードで検索すれば引っかかってきます。例えば protoc-gen-go の場合は ここ に見つかりました。

この辺は protoc で色々生成しているうちに妙に探すのが小慣れてきた感じがします。この方法の難点は、探し出したオプションが将来的に消えることはないのだろうか、と心配になるところですね。

TypeScript の型定義を観点に gRPC-Web の実装ライブラリを比較する

gRPC-Web の実装を調べてみると、どうやら複数の実装があるみたいです。 それぞれ関連ライブラリの組み合わせが決まっており、きちんと区別しないと混乱しそうなのでまとめてみます。

比較のために作成したプロジェクトは以下に置いてあります。

gRPC-Web の実装を簡易比較

  • grpc/grpc-web (star: 700)
    • grpc のオーガニゼーション(以下本家)にある...と思ってこれを選ぶとまだ成熟しておらず後悔する
    • 特に TypeScript の型定義は完全にサポートされていない
      • アクセッサの戻り型が {} で潰されていたりする
  • improbable-eng/grpc-web (star: 1.7k)
    • 本家よりも勢いがあって利用者も多そう
    • TypeScript の型定義は本家よりも充実している

なので gRPC の本家本元のリポジトリにも grpc/grpc-web という実装はあるけれど、 improbable-eng/grpc-web の方が人気があり実装状況もマルという状況ですね。

以下、両者を細かく見ていきます。 比較の題材にした Protobuf 定義は以下の通りです。

syntax = "proto3";

package hello;

service Greeting {
  rpc Hello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
  string message = 1;
}

message HelloResponse {
  string message = 1;
  Profile profile = 2;
}

message Profile {
  string name = 1;
}

grpc/grpc-web

本家お膝元のリポジトリです。 しかしもう一つのリポジトリと比較すると実装に開きがあります。

protoc \
  --js_out=import_style=commonjs:${OUT_DIR} \
  --grpc-web_out=import_style=commonjs,mode=grpcwebtext:${OUT_DIR} \
  hello.proto

のように生成します。 これで以下のようなファイルが生成されます。

out/grpc-web-by-grpc/proto
├── hello_grpc_web_pb.d.ts
├── hello_grpc_web_pb.js
├── hello_pb.d.ts
└── hello_pb.js

上で書いた「TypeScript の型が {} で潰される」とは以下のように Profile 型が返ってきてほしいところが {} になっているところを指しています。

export class HelloResponse {
  constructor ();
  getMessage(): string;
  setMessage(a: string): void;
  getProfile(): {};
  setProfile(a: {}): void;
  serializeBinary(): Uint8Array;
  static deserializeBinary: (bytes: {}) => HelloResponse;
}

おそらくまだ実装途中なのだと思います。 toObject の型が定義されていないのも物足りないところですね。

improbable-eng/grpc-web

本家よりも勢いのあるリポジトリです。

protoc \
    --js_out="import_style=commonjs,binary:${OUT_DIR}" \
    --ts_out="service=true:${OUT_DIR}" \
    hello.proto

のように生成します。これで以下のようなファイルが生成されます。

out/grpc-web-by-improbable-eng/proto
├── hello_pb.d.ts
├── hello_pb.js
├── hello_pb_service.d.ts
└── hello_pb_service.js

微妙にファイルの命名が違っていますが、hello_pb.js の型付けのために hello_pb.d.ts が生成されているのは同じですね。 ただこちらの方が Message の型付け度合いは以下のように充実しています。

export class HelloResponse extends jspb.Message {
  getMessage(): string;
  setMessage(value: string): void;

  hasProfile(): boolean;
  clearProfile(): void;
  getProfile(): Profile | undefined;
  setProfile(value?: Profile): void;

  serializeBinary(): Uint8Array;
  toObject(includeInstance?: boolean): HelloResponse.AsObject;
  static toObject(includeInstance: boolean, msg: HelloResponse): HelloResponse.AsObject;
  static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
  static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
  static serializeBinaryToWriter(message: HelloResponse, writer: jspb.BinaryWriter): void;
  static deserializeBinary(bytes: Uint8Array): HelloResponse;
  static deserializeBinaryFromReader(message: HelloResponse, reader: jspb.BinaryReader): HelloResponse;
}

toObject にも型が設定されているのが嬉しいですね。現状で選ぶならこちら一択だと思います。

protoc のオプション js_out の提供元

余談的なものになりますが、ts-protoc-gen や protoc-gen-grpc-web を比較しているときに両者が共通して利用している --js_out が気になりました。 protoc のデフォルトのオプションに含まれているので、元から含まれているものでしょうか。 となると JavaScript のコードジェネレータの実装も protoc に含まれているということなのでしょうか。

https://github.com/protocolbuffers/protobuf/tree/master/js がそれっぽいですね。

ただ gRPC のコードは含まれておらず単純にバイナリと JavaScript のデータの変換処理のみが行われているようです。 具体的には Protobuf の Message ごとに以下のメソッドを生やしているようです。

  • toObject
  • deserializeBinary
  • deserializeBinaryFromReader
  • serializeBinary
  • serializeBinaryToWriter

これ以外の処理は生成されないようなので完全にデータ変換のみの実装ですね。 なので protoc に含まれている JavaScript のコードジェネレータで生成されない gRPC 関係の service/server/client などの実装は ts-protoc-gen や protoc-gen-grpc-web などが独自に生成している、という理解をしました。

配列型のJSONから gRPC(protobuf) 用のモックデータを読み込む

gRPC のハンズオンがてら適当なサーバを書いてみて、ある程度ハンドラが充実してきたので今度は適当なデータをJSONで用意して読み込んでサーバで返すようにしよう、としたときに意外と大変だったという記録です。

動作するサーバコードは以下のリポジトリに置いてあります。

色々試したところそもそもやっていることの筋が悪いような気がしたのですが、一応動いたは動いたので記録として残しておきます。

サーバ・データ定義

サービス定義

サービス定義は以下のようなよくある ToDo アプリです。

syntax = "proto3";

import "google/protobuf/timestamp.proto";

service ReadOnlyTodo {
  rpc GetTodos(GetTodosRequest) returns (GetTodosResponse) {}
}

message Todo {
  int32 id = 1;
  string title = 2;
  google.protobuf.Timestamp deadline = 3;
}

message GetTodosRequest {
}

message GetTodosResponse {
  repeated Todo todos = 1;
}

サーバ定義

サーバ定義は以下のようにしています。

type todoServer struct {
    todos []*pb.Todo
    mu    sync.Mutex
}

今回はこの todosJSONのデータをロードするための関数を生やそうと試行錯誤しています。ロードするJSONは以下のようなものです。

[
  {
    "id": 1,
    "title": "First ToDo",
    "deadline": "2018-09-01T12:34:56.789Z"
  },
  {
    "id": 2,
    "title": "Second ToDo",
    "deadline": "2018-09-01T12:34:56.789Z"
  }
]

配列形式で複数のエントリが表されています。[ptypes]

試したパターン

実際に試した順番からはちょうど逆転しますが成功したパターンからご紹介します。

成功したパターン

まずは成功したパターンです。

golang/protobuf#675 のコメントを参考にして書きました。 普段の json.Unmarshal と比較するととても複雑なことをしているように見えます。

func (s *todoServer) loadTodos() {

    jsonBytes, _ := ioutil.ReadFile(dataFile)
    jsonString := string(jsonBytes)
    jsonDecoder := json.NewDecoder(strings.NewReader(jsonString))

    // read open bracket
    if _, err := jsonDecoder.Token(); err != nil {
        log.Fatal(err)
    }

    for jsonDecoder.More() {
        todo := pb.Todo{}
        if err := jsonpb.UnmarshalNext(jsonDecoder, &todo); err != nil {
            log.Fatal(err)
        }
        s.todos = append(s.todos, &todo)
    }
}

Unmarshal に標準の encoding/json ではなく protobuf/jsonpb というパッケージを利用しています。 叩いたところ無事データが返ってきました。

$ grpc_cli call localhost:10000 GetTodos ""
connecting to localhost:10000
todos {
  id: 1
  title: "First ToDo"
  deadline {
    seconds: 1535805296
    nanos: 789000000
  }
}
todos {
  id: 2
  title: "Second ToDo"
  deadline {
    seconds: 1535805296
    nanos: 789000000
  }
}

直感的に jsonpb を使った場合

いちいち UnmarshalNext などせずに配列型を直接データの格納先に指定してやればいいのではと思い試してみました。

func (s *todoServer) loadTodos_byInstinctiveJSONPBUsage() {

    reader, _ := os.Open(dataFile)

    if err := jsonpb.Unmarshal(reader, &s.todos); err != nil {
        log.Fatal(err)
    }
}

これを実行すると...というよりコンパイルの時点でエラーが出て

*[]*todo.Todo does not implement "github.com/golang/protobuf/proto".Message (missing ProtoMessage method)

と言われます。つまり pb.TodoMessage の interface を満たしているけれど []pb.Todo には ProtoMessage のメソッドが存在しないので jsonpb.Unmarshalシグネチャ違反ですよと言われているんですね。

標準の encoding/json パッケージを利用した場合

そもそも jsonpb を利用せずに json を利用したらどうなるかです。

func (s *todoServer) loadTodos_byStandardJSONPackage() {

    jsonBytes, _ := ioutil.ReadFile(dataFile)

    if err := json.Unmarshal(jsonBytes, &s.todos); err != nil {
        log.Fatal(err)
    }
}

これを実行すると...

json: cannot unmarshal string into Go struct field Todo.deadline of type timestamp.Timestamp

と返ってきます。string 型を timestamp.Timestamp 型に Unmarshal できないと言っていますね。timestamp.Timestamp 型は Protocol Buffer の ptype (もしくは well-known type) です。

Go 自体そこまで詳しくはないのですが、Unmarshal の振る舞いを変えられたら json でもなんとかなる...んでしょうか。

Go の gRPC のシンプルな Interceptor を自作して理解を深める

Go の gPRC のシンプルな Interceptor を適当に書いたら割とすんなり動いたので基本的な部分をまとめていきます。

今回のサンプルは以下のリポジトリに置いてあります。

go-grpc-middleware にロガーなど様々なミドルウェア(= Interceptor)が用意されていますし、本番用途であればそれらメンテナンスや検証の行き届いたパッケージを使うべきだと思いますが、初見のパッケージのオプションを眺めるのも面倒なのでロガー程度ならスクラッチできないかなと思ってやってみたら意外と簡単でした。基本的に入出力さえ辻褄を合わせておけば何をやるのも自由そうです。

動機としてはもう一つ、grpcフレームワーク的なインタフェースに興味があったというのもありました。既製品のミドルウェアを使う前に基本的なインタフェースだけでも押さえておくと、開発の自由度も上がるかもしれないという目論見です。

それでは以下、今回行ったことの簡単な解説です。

Interceptor を自作して grpc.Server に組み込んで実行する

Interceptor を定義する

今回はミドルウェアとしては定番のリクエストのログを出力する Interceptor を題材にしました。Interceptor について調べる前はやっつけでメソッドごとの各ハンドラに log.Printf を突っ込んで回る作業をしていましたが、それらのコードも一掃できました。

Interceptor は func として定義します。Interceptor には種類があり、今回実装したのは grpc.UnaryServerInterceptor という stream でない値を返す際に動作する Interceptor です。

grpc.UnaryServerInterceptorシグネチャに従い、メソッド名・リクエスト・レスポンスを log として表示する Interceptor は以下のようになりました。引数のシグネチャが長いですがミドルウェア的な処理としては必要な情報が並んでいるだけですね。

func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {

    // ハンドラを実行する
    res, err := handler(ctx, req)

    // リクエスト処理のログを表示する(今回ミドルウェアでやりたかったこと ^-^)
    log.Println("My interceptor called!")
    log.Printf("%s: %v -> %v", info.FullMethod, req, res)

    // 実行結果とエラーを返す(返さないとサーバが結果を返さなくなる ^-^);
    return res, err
}

特に難しいことはやっておらず、関数評価時に渡される reqinfo などの材料をもとに処理を行っているだけです。

一点気になったのが Interceptor 中でメソッドのハンドラを評価し、結果を返さなければならないという点です。そこまでミドルウェアというものを経験したこともないのですが、こういう形式が普通なんでしょうか。Node.js の Express とはお作法が微妙に違うなという感触を得ました。

ちなみに resnil で置き換えたらサーバが空レスポンスを返すようになりました。

grpc.Server に設定する

Interceptor を ServerOption type に変換する grpc.UnaryInterceptor を通して Interceptor をサーバに設定します。

grpcServer := grpc.NewServer(
  grpc.UnaryInterceptor(loggingInterceptor),
)

ここもそこまで難解な見た目にはなっていませんね。シンプルです。

実行

サーバを実行してリクエストを送ったところ、サーバのログが以下のように表示されました。

2018/09/13 22:20:39 gRPC server starts on localhost:10000
2018/09/13 22:20:41 My interceptor called!
2018/09/13 22:20:41 /hello.Greeting/Hello: name:"asa-taka"  -> message:"Hello asa-taka, I am gentle-server"

...というこんな感じで grpc の Interceptor は自作するのも結構簡単なんだな、というお話でした。

余談

開発って

  • その分野のベストプラクティス・定番のライブラリを調べる
  • 調べるのが面倒だからとりあえず手前味噌で実装する

のバランスを探り探りで行なっていく作業だと思うのですが、技術検証フェーズでは後者を自由度高く行えると快適に思えることが多いです。ミドルウェアのインタフェースを知ることも、大いにそれに与するものだと思っております。