遂にalphaが取れた🏅grpc-swiftへの移行注意点✋やdocsに明記のない機能紹介📝

公開:2021/02/18
更新:2021/02/22
25 min読了の目安(約23000字TECH技術記事

本記事は、2021/02/13 時点の情報に基づいて執筆しています

#1 はじめに

grpc-swiftは Swift 向けのコード生成に関するプラグインが管理されているレポジトリです。protoc コマンド向けに protoc-gen-swift 及びprotoc-gen-grpc-swiftのプラグインが作られています。.protoファイルに定義されている service および、 message を Swift から呼び出せる形にコード生成して、.swift ファイルを提供してくれるものです。

この記事は誰向け?

この記事では、 gRPC-Swiftの簡単な使い方から、v0.x から v1.x への移行方法、更には内部実装まで踏み込んだ内容まで盛り込んでいます。
前半では、簡単な使い方をコードベースで紹介します。gRPC を使ったことがない人でも理解できる粒度で記事を書くことを心がけています。
後半は、内部実装を読んでいて気になった fakeChannel 周りの実装について説明します。こちらは、gRPC-Swiftそのものに興味がある人向けとなっています。

歴史

はじめに、簡単に歴史に触れます。これまで grpc-swift はgRPC C libraryをベースに実装されてきました。これは言い換えるとオリジナル実装である grpc/grpc をベースに作られたものです。この実装では

  • データの送受信のために多数のスレッドを開く必要がある。
  • 各リクエストごとにサーバは gRPC 操作キューを待つために 1 つのディスパッチスレッドしか開けない。

といった課題がありました。これを解決するために apple/swift-nio ベースの実装に置き換え、ついでに様々なインタフェースをより使いやすくすることで本レポジトリ自体を改善しようという流れが 2018/5 頃に提案され実装されてきました。ここから長く時間がかかりましたが 2021/2 についに予てより alpha バージョンであった v1.x 系の alpha がとれ、正式に 1.0.0 がリリースされました。

そのため、grpc-swift のレポジトリには旧来の grpc/grpc ベースで作成された v0.x 系と、 apple/swift-nio ベースで作成された v1.x 系の2種類が存在しています。レポジトリにはそれぞれの差分が以下のように書かれています。特にプラグインのバイナリ名とpod名が絶妙に違う名前になっているので使う際は気をつけてください。

\ v0.x v1.x
Implementation grpc/grpc apple/swift-nio
Branch cgrpc main
protoc Plugin protoc-gen-swiftgrpc protoc-gen-grpc-swift
CocoaPod SwiftGRPC gRPC-Swift
Support セキュリティパッチだけ 主だって開発・サポートされている

#2 v1.x系の簡単な使い方

ここで示すコードはこちらのレポジトリのコードを参照します。まとめてコードを読みたい方は GitHub で見て頂ければと思います。

この記事ではこのレポジトリのコードを多く参照します。

1. ライブラリを入れる

CocoaPodsSwiftPMにて、配信されています。プロジェクトで使用しているパッケージマネージャに合わせて適切な場所から入れてください。上で示したサンプルコードでは SwiftPM(をXcodeGenで指定する形)から入れています。

2. protocプラグインを用意する

protoc プラグインは、.proto ファイルから、.swift ファイルを生成するために必要なプラグインです。swift 向けのprotocプラグインである protoc-gen-grpc-swift を入れるにはいくつかの方法があります。

a. GitHubのreleaseから入れる

https://github.com/grpc/grpc-swift/releases にて配信されている protoc-grpc-swift-plugins-1.0.0.zipをインストールする方法です。

Releaseページ.

インストールすると protoc-grpc-swift-plugins-1.0.0.zip に関してはbin/の中に、protoc-gen-swift 及び protoc-gen-grpc-swiftが入っているので、bin/以下を適当にPATHの通っているところにコピーするなり、bin/にPATHを通すなりするとOKです。

b. CocoaPodsから入れる

pluginのバイナリが CocoaPods で配信されています。以下を Podfile に追記してpod installを行うことで、Pods/gRPC-Swift-Plugins/ のパスにバイナリが追加されます。

pod 'gRPC-Swift-Plugins'

プロジェクトの initialize 処理などでこのパスを参照するといいと思います。この方法は普段のプロジェクトで CocoaPods を用いてライブラリを管理している場合には、Podfile に追記を行うだけですので、非常に簡単です。

c. 直接 make する

grpc/grpc-swift のレポジトリを clone して、直接makeコマンドを叩くことでも生成が可能です。以下を始めに実行します。

git clone git@github.com:grpc/grpc-swift.git && \
cd grpc-swift && \
make plugins

すると、 protoc-gen-swift 及び protoc-gen-grpc-swift がプロジェクトのルートに生成されます。これをよしなに、PATHの通っているところにコピーする or protocコマンド実行時にパスを直接指定すると protoc コマンドのパラメータとして、--swift_out--grpc-swift_outを使用できるようになります。

3. *.grpc.swift及び*.pb.swiftの生成をする

protocコマンド周辺の準備が終われば、あとはコード生成を行うだけです。サンプルのコードではMakefile内で以下のようなコマンドを実行しています。(該当部分)

protoc proto/HelloService.proto \
  --swift_out=./swift/helloServiceClient/data/gen --swift_opt=Visibility=Public \
  --grpc-swift_out=./swift/helloServiceClient/data/gen --grpc-swift_opt=Visibility=Public,Server=false,Client=true,TestClient=true

--swift_out--grpc-swift_out のような generated なコードの出力先のパス指定以外に、--grpc-swift_optという形でオプションを指定できる事がわかると思います。
ここでは、

  • Visibility = Public (アクセスコントロールをinternalではなくPublicにする)
  • Server = false (Serverのコードを生成しない)
  • Client = true (Clientのコードを生成する)
  • TestClient = true (TestClientと呼ばれるmock動作向けのコードを生成する)

を指定しています。その他にも FileNaming(protoファイルのパス考慮の仕方)や KeepMethodCasing(protoファイルで定義してるrpcのアッパーキャメルを維持するか否か)といったオプションがあります。詳細はこちらを見てもらえればと思います。

こちらのコマンドの実行に成功するとファイルが生成されます(サンプルでの該当部分はこちら)。生成されたファイルの先頭には// DO NOT EDIT. と書かれており、まじで勝手に編集してはいけない雰囲気(?)が漂っています。

// DO NOT EDIT.
// swift-format-ignore-file
//
// Generated by the Swift generator plugin for the protocol buffer compiler.
// Source: proto/HelloService.proto
//
// For information on using the generated types, please see the documentation:
//   https://github.com/apple/swift-protobuf/

4. 生成コードを実際に使用する

生成されたコードを使用するには、大まかには、1. Clientの作成、2. requestを詰める、3. responseを受け取る のフローを踏む必要があります。こちらについて示します。

1. Clientの作成

Client の作成に必要なファイルは全て自動生成によって作成されます。つまり、*.grpc.swiftの中に Client ファイルが生成されています。例示しているプロジェクトではこのようなものこのようなものが生成されています。この Client を介して request を実際に送るという流れです。 Client の生成に required なパラメータとして、 GRPCChannel があります。

/// Creates a client for the pb.HelloService service.
///
/// - Parameters:
///   - channel: `GRPCChannel` to the service host.
///   - defaultCallOptions: Options to use for each service call if the user doesn't provide them.
///   - interceptors: A factory providing interceptors for each RPC.
public init(
    channel: GRPCChannel, // required!!!
    defaultCallOptions: CallOptions = CallOptions(), // defaultが詰められている
    interceptors: Pb_HelloServiceClientInterceptorFactoryProtocol? = nil // defaultでnilが詰められている
) {
    self.channel = channel
    self.defaultCallOptions = defaultCallOptions
    self.interceptors = interceptors
}

GRPCChannelClientConnection を介して作成します。 basic-tutorial には以下のようなコードが書いてあります。TLS を使用しないようなものは .insecure でビルダーを作成し、 .connecthost: port: を指定することで、 channel が作成できます。 MultiThreadedEventLoopGroup は Swift-NIO にて定義されているもので、詳細は省きますが thread 数として 1 や System.coreCount が多く指定されています。

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer {
  try? group.syncShutdownGracefully()
}

let channel = ClientConnection.insecure(group: group)
  .connect(host: "localhost", port: port)

このようにして作成した channel を Client の引数に渡すことで、接続が可能になります。

2. requestを詰める

先述のセクションで作成した Client を用いて request を送ります。 Client は先述の通り、prtoc によって自動生成されているので、送りたい rpc へのメソッドも自動で生成されます。例示しているプロジェクトの rpc の一つである hello は以下のように生成されています。(リンク1) (リンク2)

/// Unary call to Hello
///
/// - Parameters:
///   - request: Request to send to Hello.
///   - callOptions: Call options.
/// - Returns: A `UnaryCall` with futures for the metadata, status and response.
public func hello(
    _ request: Pb_HelloRequest,
    callOptions: CallOptions? = nil
) -> UnaryCall<Pb_HelloRequest, Pb_HelloResponse> {
    return self.makeUnaryCall(
        path: "/pb.HelloService/Hello",
        request: request,
        callOptions: callOptions ?? self.defaultCallOptions,
        interceptors: self.interceptors?.makeHelloInterceptors() ?? []
    )
}

クライアントの引数に Pb_HelloRequest 型のリクエストを詰めればいいことがわかります。
詰めるときは以下みたいにwithを使うと便利です。

let request = Pb_HelloRequest.with {
    $0.name = message
}
let call = self.client.hello(request, callOptions: nil)

3. responseを受け取る

以下のようなコードで let response = try call.response.wait() として、シュッと書くことで取れます。 (例示プロジェクトでの該当箇所)

do {
    let call = self.client.hello(request, callOptions: nil)
    let response = try call.response.wait()
    print(response.hello)
} catch {
    print(error)
}

responseEventLoopFuture<value> という型で返ってきます。こちらもここまで読んでいる勘の良い読者の方はもうわかっていると思いますが、こちらは swift-NIO で定義されているものです。 EventLoopFuture は上記のように .wait() としても取得できますし、 .whenSuccess , .whenComplete , .whenFailure といったメソッドも生えており、コールバック的にも取得できます。[1] また、取得した値はRx-likeに、 .flatMap によって変換も可能です。

#3 v0.xからの移行

これまで、 v1.x 系が alpha バージョンだったこともあり、従来の v0.x 系を使い続けていた方も多いのではないかと思います。この章では、簡単な差分・移行手順について説明したいと思います。

importするもの

v0.x 、言い換えると、過去バージョンの protoc ライブラリであるprotoc-gen-swiftgrpcでライブラリ依存解決を行うと、インポートされるものは import SwiftGRPC というものです。これがサジェストされる場合には新しいライブラリに変更できていない可能性が高いです。また、 v0.x では SwiftGRPC だけをインポートすればよいのですが、 v1.x では何度も記述している通り swift-NIO への依存があるため、状況によっては import NIO 等が必要です。

import差分 v0.x v1.x
インポートされるもの SwiftGRPC GRPC
他依存 特になし NIO

Client周り

Client は v0.x 系では単に Client に address 及び、 tls を用いるかどうかの Bool を渡すだけでしたが、 v1.x では swiftNIO の EventLoopGroupなどを用いて client の作成を行う必要があります。詳細は #2.4 生成コードを実際に使用する での紹介を参考にしていただければと思います。

Metadata周り

gRPC には Metadata といういわゆる header というか option というかそういったものを扱うことができます。 v0.x 系では、ダイレクトに Metadata という型が存在していました。 v1.x ではこれが CallOptions という名前に変わっており、従来よりも安全に扱いやすくなっています。以下がコード例です。オプションに user-id というものを詰めるとしたときの v0.x、 v1.x での記述方法です。

v0.xでの例

let metadata = try Metadata(["user-id": userId])

v1.xでの例

var callOptions = CallOptions()
callOptions.customMetadata.add(contentsOf: [("user-id", userId)])

StatusCode周り

大きくは変わってないのですが、型名が StatusCode から GRPCStatus.Code に変わっています。 v1.x からは GRPCStatus というやつがいい感じに見てくれるということですね。
過去に error だったときの status code を取るときの記述にも差分があったので、例を示しておきます。

v0.xでの例

private func getStatusCode(_ error: Error) -> StatusCode? {
    guard let rpcError = error as? RPCError else {
        return nil
    }
    guard let statusCode = rpcError.callResult?.statusCode else {
        return nil
    }
    return statusCode
}

v1.xでの例

private func getStatusCode(_ error: Error) -> GRPCStatus.Code? {
    guard let rpcError = error as? GRPCStatus else {
        return nil
    }
    return rpcError.code
}

シンプルになった?ような

#4 mockのための振る舞いであるTestClientとfakeChannel

protoc-gen-grpc-swift の option には、TestClient というものがあります。 Default は false なのですがこの option は結構便利なので紹介したいと思います。またそれがどいった実装で実現されているのかについても触れたいと思います。ここからはgrpc-swiftオタク向けですw

TestClientの使い方

mock 動作時や UnitTest 時に実際にサーバにリクエストを送ることは、あまり良くないです(良くないというか、そんな事あんまりないような)。そこで、実際にリクエストを送らずに定義された proto ファイルの message に合わせて予め固定のデータを送ることができます。この振る舞いを持つのが TestClient です。これは *.grpc.swift ファイルにオプションを指定して protoc コマンドを実行した場合に生成されます。例示しているプロジェクトではこの辺りです。オプションの指定方法は --grpc-swift_opt=TestClient=true です。

コード例としては以下のような感じです。例示しているプロジェクトではこの辺りです。プロジェクトでは DI で差し替えており、mock ビルドのときには TestClientを、その他の場合は通常の Client を使用するようにしています。 TestClient に mock 動作時に返却して欲しい値を enqueue するようなメソッドが生成されているためこれを利用します。

let testClient = Pb_HelloServiceTestClient()
// mockのデータを詰める
testClient.enqueueHelloResponse(Pb_HelloResponse.with {
    $0.hello = "mock message"
})

// ここは client が通常の場合と同じように扱うことができる
do {
    let request = ... // 適当なものを入れる
    let call = testClient.hello(request, callOptions: nil)
    let response = try call.response.wait()
    print(response.hello) // 必ず "mock message" が出力される
} catch {
    print(error)
}

注意する点は、 enqueue した回数だけしか呼び出すことができません。それ以上呼び出すとエラーとなるため注意しましょう。逆に言えば、意図しない回数の呼び出しなどもテスト可能です。

Clientの差し替えを実現する仕組み

この Mock や UnitTest 向けに有用な option である TestClient はどのようにして実装されているのでしょうか? 生成される Client は protocol GRPCClient を実装していますが、直接実装しているのではなく間に *.grpc.swift にて一つ protocol を挟んでいます。

これまで例示していたプロジェクトで具体例を挙げると、protocol Pb_HelloServiceClientProtocolprotocol GRPCClientを継承しており、Pb_HelloServiceClientPb_HelloServiceTestClientprotocol Pb_HelloServiceClientProtocol を実装している形になっています。図で示すと以下のような形です。

GRPCClientを実装するクラスでは、init 時に channel: GRPCChannelが必要です。 Pb_HelloServiceClient を作成する際には、やり取りするエンドポイントやポート情報などを ClientConnectionを通じて channel を作成し、渡しているはずです。一方で、Pb_HelloServiceTestClient は単に Pb_HelloServiceTestClient() とインスタンス化するだけで使用できています。内部では一体何が channel に渡されているのでしょうか?

答えは FakeChannel です。例示しているプロジェクトではこのようにして渡し、参照する際にはこのように fakeChannel を返却している様子がわかります。

public final class Pb_HelloServiceTestClient: Pb_HelloServiceClientProtocol {
    private let fakeChannel: FakeChannel
    public var defaultCallOptions: CallOptions
    public var interceptors: Pb_HelloServiceClientInterceptorFactoryProtocol?

    public var channel: GRPCChannel {
        return self.fakeChannel
    }

    public init(
        fakeChannel: FakeChannel = FakeChannel(),
        defaultCallOptions callOptions: CallOptions = CallOptions(),
        interceptors: Pb_HelloServiceClientInterceptorFactoryProtocol? = nil
    ) {
        self.fakeChannel = fakeChannel
        self.defaultCallOptions = callOptions
        self.interceptors = interceptors
    }
    
    // ...

寄り道

間に Pb_HelloServiceClientProtocol を挟むことによって proto ファイルで定義した message を持つ Client として適度な粒度で実装を縛り、 Client or TestClient をいい感じに選択できるようになっています。実際に、プロジェクト例では DI の中でこれを差し替えるような実装をしています(ここ)。

container.register(Pb_HelloServiceClientProtocol.self) { _ in
    let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    #if MOCK
    let testClient = Pb_HelloServiceTestClient()
    testClient.enqueueHelloResponse(Pb_HelloResponse.with {
        $0.hello = "mock message"
    })
    return testClient
    #else
    let channel = ClientConnection
        .insecure(group: group)
        .connect(host: "REQUEST_HOST_NAME", port: 5300)
    return Pb_HelloServiceClient(
        channel: channel
    )
    #endif

FakeChannel

さて、TestClient の option に true を指定したときには TestClient が生成され、その内部では FakeChannel() が指定されることで実現していることをここまで説明しました。では、どうやって mock データを内部で保持し、mock されたデータを返却しているのかについて深追いしてみたいと思います。

Enqueue

TestClientに mock のデータを enqueue する関数は以下のようになっています。

public func makeHelloResponseStream(
    _ requestHandler: @escaping (FakeRequestPart<Pb_HelloRequest>) -> () = { _ in }
) -> FakeUnaryResponse<Pb_HelloRequest, Pb_HelloResponse> {
    return self.fakeChannel.makeFakeUnaryResponse(path: "/pb.HelloService/Hello", requestHandler: requestHandler)
}

public func enqueueHelloResponse(
    _ response: Pb_HelloResponse,
    _ requestHandler: @escaping (FakeRequestPart<Pb_HelloRequest>) -> () = { _ in }
)  {
    let stream = self.makeHelloResponseStream(requestHandler)
    // This is the only operation on the stream; try! is fine.
    try! stream.sendMessage(response)
}

どうやら FakeUnaryResponse<Pb_HelloRequest, Pb_HelloResponse> に対して sendMessageする形を取っているようです。makeFakeUnaryResponseの実装を覗いてみましょう(ここ)。

public func makeFakeUnaryResponse<Request, Response>(
    path: String,
    requestHandler: @escaping (FakeRequestPart<Request>) -> Void
) -> FakeUnaryResponse<Request, Response> {
    let proxy = FakeUnaryResponse<Request, Response>(requestHandler: requestHandler)
    self.responseStreams[path, default: []].append(proxy)
    return proxy
}

FakeUnaryResponse という response を作ってそれを、 FakeChannel 内部に保持している responseStreams という Dictionary に詰めているようです。この辞書型は、key が grpc の pathで、valueが循環バッファになっています。つまり、詰めたデータはFIFOに取り出されることになります。

循環バッファに詰められた stream はその後 sendMessageされるのですが、この関数を今度は眺めてみましょう(ここ)。

public func sendMessage(
    _ response: Response,
    initialMetadata: HPACKHeaders = [:],
    trailingMetadata: HPACKHeaders = [:],
    status: GRPCStatus = .ok
) throws {
    try self._sendResponsePart(.initialMetadata(initialMetadata))
    try self._sendResponsePart(.message(.init(response, compressed: false)))
    try self._sendResponsePart(.trailingMetadata(trailingMetadata))
    try self._sendResponsePart(.status(status))
}

4種類のものについて実際のリクエストの送信を模したものを行っているようです。ここを見ると、通常のオプションで吐き出した enqueueHelloResponse のような enqueue を行うメソッドでは正常系しか基本的には詰め込むことができませんが、_FakeResponseStream.swift 内の sendMessage を自分で叩くようにしたら、いろんな状態を試すことができそうです。(例えば status = .processingError にするとか)
また、Error を再現するには sendMessage ではなく、 sendError を用いることでもっとシンプルにできそうです (ここ)。

self._sendResponsePart を更に深掘ってみましょう。ここは何ステップかありますが、流れとしては 1. send, 2. validate, 3. write のようになっています。


internal func _sendResponsePart(_ part: _GRPCClientResponsePart<Response>) throws {
    try self.send(.responsePart(part))
}

private func send(_ event: StreamEvent) throws {
    switch self.validate(event) {
    case .valid:
        self.writeOrBuffer(event)

    case let .validIfSentAfter(extraPart):
        self.writeOrBuffer(extraPart)
        self.writeOrBuffer(event)

    case let .invalid(reason):
        throw FakeResponseProtocolViolation(reason)
    }
}

1.send はこれまで説明した部分です。2.validate では各 stream のステータスごとに状態を検査しています(ここ)。3.write ではやり取りを行う Channel [2]が active であれば実際に書き込み、 inactive ではファイル内で保持している CircularBuffer に書き込んで stream をバッファするという処理を行っています。その後実際に stream が activate されたら、バッファしているものを一気に流し込んでいるようです (ここ)。

internal func activate() {
    switch self.activeState {
    case .inactive:
        // Activate the channel. This will allow any request parts to be sent.
        self.channel.pipeline.fireChannelActive()

        // Unbuffer any response parts.
        while !self.responseBuffer.isEmpty {
            self.write(self.responseBuffer.removeFirst())
        }

        // Now we're active.
        self.activeState = .active

    case .active:
        ()
    }
  }

Call

次は呼び出し時の動きです。 TestClient からの呼び出しに関しても protocol で実装を縛ってあるので、呼び出すメソッドは変わりません。サンプルのプロジェクトでの例を示すと以下のようになっています(ここ)。

let call = self.client.hello(request, callOptions: nil)
let response = try call.response.wait()

ここから一体どのようにして、先程 enqueue した mock data までたどり着くのでしょうか?
*.grpc.swift の func hello を見てみましょう。extensionの中に以下のようなコードがあります。

public func hello(
    _ request: Pb_HelloRequest,
    callOptions: CallOptions? = nil
) -> UnaryCall<Pb_HelloRequest, Pb_HelloResponse> {
    return self.makeUnaryCall(
      path: "/pb.HelloService/Hello",
      request: request,
      callOptions: callOptions ?? self.defaultCallOptions,
      interceptors: self.interceptors?.makeHelloInterceptors() ?? []
    )
}

どうやら makeUnaryCall を読んでいるようですね。さらに中を覗いてみましょう(ここ)。

public func makeUnaryCall<Request: GRPCPayload, Response: GRPCPayload>(
    path: String,
    request: Request,
    callOptions: CallOptions? = nil,
    interceptors: [ClientInterceptor<Request, Response>] = [],
    responseType: Response.Type = Response.self
) -> UnaryCall<Request, Response> {
    return self.channel.makeUnaryCall(
      path: path,
      request: request,
      callOptions: callOptions ?? self.defaultCallOptions,
      interceptors: interceptors
    )
}

clientの保持する channel の makeUnaryCall を呼び出していますね。
この、makeUnaryCall が何を呼び出しているかを更に掘ります(ここ)。

public func makeUnaryCall<Request: Message, Response: Message>(
    path: String,
    request: Request,
    callOptions: CallOptions,
    interceptors: [ClientInterceptor<Request, Response>] = []
) -> UnaryCall<Request, Response> {
    let unary: UnaryCall<Request, Response> = UnaryCall(
        call: self.makeCall(
            path: path,
            type: .unary,
            callOptions: callOptions,
            interceptors: interceptors
        )
    )
    unary.invoke(request)
    return unary
}

channel の保持する makeCall を呼んでいますね。
勘の良い読者はすでにお気づきだと思いますが、 FakeChannel ではこの makeCall を override することで、振る舞いを Fake 用に変えています。この辺は始めに protcol をいい感じに切ったおかげで実現されていることがよくわかります。

FakeChannel.swift 内の makeCall を深掘ってみましょう(ここ)。

private func _makeCall<Request: Message, Response: Message>(
    path: String,
    type: GRPCCallType,
    callOptions: CallOptions,
    interceptors: [ClientInterceptor<Request, Response>]
) -> Call<Request, Response> {
    let stream: _FakeResponseStream<Request, Response>? = self.dequeueResponseStream(forPath: path)
    let eventLoop = stream?.channel.eventLoop ?? EmbeddedEventLoop()
    return Call(
        path: path,
        type: type,
        eventLoop: eventLoop,
        options: callOptions,
        interceptors: interceptors,
        transportFactory: .fake(stream, on: eventLoop)
    )
}


private func dequeueResponseStream<Stream>(
    forPath path: String,
    as: Stream.Type = Stream.self
  ) -> Stream? {
    guard var streams = self.responseStreams[path], !streams.isEmpty else {
        return nil
    }

    // This is fine: we know we're non-empty.
    let first = streams.removeFirst()
    self.responseStreams.updateValue(streams, forKey: path)

    return first as? Stream
}

self.dequeueResponseStream は 先述した FakeChannel 内部で Dictionaly 型で保持しているmock data を取り出す操作です。ここでつながった!!! これ以降の Call型に変換してやる処理はまた通常の Channel と共通です。ただし、transportFactory は FakeChannel からの呼び出しの場合は .fake を選択しています。.http2 を用いるの通常のフローです。

この章のまとめ

FakeChannel の振る舞いとそれを実現するための仕組み、Enqueue時・Call時について深堀りしてみました。 コードを読んでいくことで、TestClient optionを指定して自動で吐き出したコード以外の状態をテストする方法などがわかって非常にヨカッタ(小並感)

#5 まとめ

本記事では、grpc-swiftについて、

  • 初めて触る人向け
  • v0.x から移行したい人向け
  • FakeChannelの振る舞いが気になる人向け

とたくさんの視点から記事を書いてみました。Swift-NIO ベースの実装に置き換わったことで今後も NIO の恩恵を受けて安定なライブラリとなったのではないでしょうか?
ぜひ、grpc を Swift から触るときには使ってみてはいかがでしょうか?

脚注
  1. https://apple.github.io/swift-nio/docs/current/NIO/Classes/EventLoopFuture.html#/flatMap and map ↩︎
  2. 注意 : ここでのChannelはいわゆるGRPCChannelではなくSwift-NIOのEmbeddedChannelです ↩︎