遂にalphaが取れた🏅grpc-swiftへの移行注意点✋やdocsに明記のない機能紹介📝
本記事は、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. ライブラリを入れる
CocoaPods
、SwiftPM
にて、配信されています。プロジェクトで使用しているパッケージマネージャに合わせて適切な場所から入れてください。上で示したサンプルコードでは 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をインストールする方法です。
インストールすると 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 に追記を行うだけですので、非常に簡単です。
make
する
c. 直接 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
を使用できるようになります。
*.grpc.swift
及び*.pb.swift
の生成をする
3. 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
}
GRPCChannel
は ClientConnection
を介して作成します。 basic-tutorial には以下のようなコードが書いてあります。TLS を使用しないようなものは .insecure
でビルダーを作成し、 .connect
で host:
や 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)
}
response
は EventLoopFuture<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_HelloServiceClientProtocol
がprotocol GRPCClient
を継承しており、Pb_HelloServiceClient
や Pb_HelloServiceTestClient
は protocol 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 から触るときには使ってみてはいかがでしょうか?
Discussion