Slide 1

Slide 1 text

共通処理の挟み方 2019/06/04 MagicOnion勉強会 ぱすた(@p_a_sta) 1

Slide 2

Slide 2 text

だれ? ぱすた(@p_a_sta) 株式会社Donuts 新規開発部 音ゲーつくったり基盤フレームワークつくったり サーバサイド初心者 2

Slide 3

Slide 3 text

注意 今回は非ストリーミングAPIしか扱いません。 (ごめんなさい) 3

Slide 4

Slide 4 text

やりたいこと ● サーバー/クライアント ○ 共通リクエスト/レスポンスの送受信 ○ 暗号化 ● クライアント ○ 共通エラーハンドリング ○ リトライ ○ デバッグ出力 などなど…… 4

Slide 5

Slide 5 text

サーバーサイド 5

Slide 6

Slide 6 text

サーバーサイド 2通りのやりかた ● MagicOnionFilter ○ MagicOnionに用意された機能 ● gRPC Interceptor ○ gRPC本来の機能 どちらもRPCの前後に処理を挟むもの 6

Slide 7

Slide 7 text

MagicOnionFilter - 実装方法 MagicOnionFilterAttributeを継承する public class SampleFilterAttribute : MagicOnionFilterAttribute { public override async ValueTask Invoke(ServiceContext context) { try { /* 前処理 */ await Next(context); /* 後処理 */ } catch { /* エラー処理 */ throw; } finally { /* on finally */ } } } 7

Slide 8

Slide 8 text

MagicOnionFilter - 適用方法 ①特定のクラス全体・または1つのメソッドに適用する場合 クラスまたはメソッドに属性として付与する // クラスに付けるとクラス全体に適用される [SampleFilter] public class SampleService : ServiceBase, ISampleService { // メソッドに付けるとメソッドだけに適用される [SampleFilter] public async UnaryResult HogeAsync() { } } 8

Slide 9

Slide 9 text

MagicOnionFilter - 適用方法 ②全ての通信に適用する場合 MagicOnionOptions で指定する var options = new MagicOnionOptions { GlobalFilters = new[] { new SampleFilterAttribute() }, }); var service = MagicOnionEngine.BuildServerServiceDefinition(options); 9

Slide 10

Slide 10 text

MagicOnionFilter - 概要図 10

Slide 11

Slide 11 text

gRPC Interceptor - 実装方法 Grpc.Core.Interceptors.Interceptor を継承する サーバ側・クライアント側それぞれについて、RPC 種類ごとにメ ソッドが用意されているので、必要なものだけを override して実 装する 今回は UnaryServerHandler のみ説明 11

Slide 12

Slide 12 text

gRPC Interceptor - 実装方法 public class SampleInterceptor : Interceptor { // ※MagicOnionを使っている限り TRequest, TResponse は byte[] 固定 public override async Task UnaryServerHandler( TRequest request, // 生リクエストデータ ServerCallContext context, UnaryServerMethod continuation ) { /* 前処理 */ var response = await continuation(request, context); /* 後処理 */ return response; } } 12

Slide 13

Slide 13 text

gRPC Interceptor - 適用方法 MagicOnionServiceDefinition.ServerServiceDefinition から Interceptor を刺した ServerServiceDefinition を生成する var service = MagicOnionEngine.BuildServerServiceDefinition(); var serverService = service.ServerServiceDefinition.Intercept( new SampleInterceptor() ); 13

Slide 14

Slide 14 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 14

Slide 15

Slide 15 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.Items がリクエストごとに固有 のストレージになっている 15

Slide 16

Slide 16 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.RequestHeaders と context.WriteResponseHeadersAsync を使う 16

Slide 17

Slide 17 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.CallContext で gRPC Interceptor 側の context にアクセス可能 17

Slide 18

Slide 18 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 18

Slide 19

Slide 19 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ context.ForceSetRawUnaryResult で書き換えは可能 元の値は取得不可 19

Slide 20

Slide 20 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 20

Slide 21

Slide 21 text

比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯ ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 21 要件に合う方を選んで使うのが重要

Slide 22

Slide 22 text

サンプル - 共通リクエスト/レスポンスの送受信 ● 要件 ○ メソッド本体との値のやり取り ○ リクエストヘッダの読みとり ○ レスポンスヘッダの書き換え MagicOnionFilter 22

Slide 23

Slide 23 text

public class CommonFilterAttribute : MagicOnionFilterAttribute { // ヘッダにバイナリを詰める時は Metadata.BinaryHeaderSuffix を付けないといけない public const string Key = "base" + Metadata.BinaryHeaderSuffix; public override async ValueTask Invoke(ServiceContext context) { var header = context.CallContext.RequestHeaders.First(x => x.Key == Key); var commonRequest = LZ4MessagePackSerializer.Deserialize(header.ValueBytes); /* 共通リクエストに対する処理 */ context.Items[Key] = commonRequest; // メソッドで使うために詰めておく await Next(context); var commonResponse = new CommonResponse { /* 共通レスポンス作成 */ }; await context.CallContext.WriteResponseHeadersAsync(new Metadata { { Key, LZ4MessagePackSerializer.Serialize(commonResponse) } }); } } 23

Slide 24

Slide 24 text

サンプル - 暗号化 ● 要件 ○ リクエストボディの書き換え ○ レスポンスボディの書き換え ○ クライアントと共通化 gRPC Interceptor 24

Slide 25

Slide 25 text

public class CryptoInterceptor : Interceptor { public const string Key = "暗号鍵"; public override async Task UnaryServerHandler( TRequest request, ServerCallContext context, UnaryServerMethod continuation ) { request = Decrypt(request as byte[]) as TRequest; var response = await continuation(request, context); return Encrypt(response as byte[]) as TResponse; } private byte[] Encrypt(byte[] rawData) => 実装略; private byte[] Decrypt(byte[] encryptedData) => 実装略; } 25

Slide 26

Slide 26 text

クライアントサイド 26

Slide 27

Slide 27 text

クライアントサイド - 実装方法 ● 基本的に gRPC Interceptor 一択 ● MagicOnionClient.Create に Interceptor を挟んだ CallInvoker を渡す var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure); var invoker = channel.Intercept(new HogeInterceptor()); var client = MagicOnionClient.Create(invoker); 27

Slide 28

Slide 28 text

クライアントサイド - 実装方法 28 public class SampleInterceptor : Interceptor { public override AsyncUnaryCall AsyncUnaryCall( TRequest request, ClientInterceptorContext context, AsyncUnaryCallContinuation continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } }

Slide 29

Slide 29 text

クライアントサイド - 実装方法 29 public class SampleInterceptor : Interceptor { public override AsyncUnaryCall AsyncUnaryCall( TRequest request, ClientInterceptorContext context, AsyncUnaryCallContinuation continuation ) { /* 前処理 */ var call = continuation(request, context); return call; } } 戻り値が Task(or task-like) じゃない →async/awaitが使えない →後処理どうするの?

Slide 30

Slide 30 text

クライアントサイド - 実装方法 30 var call = continuation(request, context); return new AsyncUnaryCall( call.ResponseAsync.ContinueWith(res => { /* 後処理 */ return res.Result; }), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); ● 新しい AsyncUnaryCall を作る

Slide 31

Slide 31 text

クライアントサイド - 実装方法 31 var call = continuation(request, context); return new AsyncUnaryCall( call.ResponseAsync.ContinueWith(async res => { /* 後処理 */ return res.Result; }).Unwrap(), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); ● 後処理で await したい場合

Slide 32

Slide 32 text

クライアントサイド - 実装方法 32 ● 前処理で await したい場合 ○ 不明……

Slide 33

Slide 33 text

エラーハンドリング 33

Slide 34

Slide 34 text

エラーハンドリング - 落とし穴 ● 例外が発生した場合 ○ gRPC 的には RpcException が発生するはず ○ 実際は AggregateException が発生 ■ Task 内部で発生した例外は AggregateException に 集約される ■ 元の例外は .InnerExceptions で取得 34

Slide 35

Slide 35 text

エラーハンドリング - 落とし穴 ● 例外が発生した場合 ○ Interceptor を重ねるとその数だけ AggregateException でラップされる ■ めちゃくちゃ取り出しづらい ■ 本来は機能単位で Interceptor を分けるべきだが、例 外が扱いづらくなるため1クラスにまとめたほうが良い かもしれない ● 結局サーバーと共通化できない 35

Slide 36

Slide 36 text

エラーハンドリング - リトライ ● 一定の例外だったらリトライしたい ● Interceptor でリトライをする方法はなさそう(※独自調べ) 拡張メソッドを作って、必ずそこ経由で呼ぶようにするしかなさそう 36

Slide 37

Slide 37 text

エラーハンドリング - リトライ 37 // InvokeWithErrorAsync 経由で呼ぶ MagicOnionClient.Create().InvokeWithErrorAsync(x => x.HogeAsync()); // 通常はこう // MagicOnionClient.Create().HogeAsync(); ● イメージ (めんどくさい……)

Slide 38

Slide 38 text

リトライ - 実装方法 38 // IService からメソッドチェーンしたいので型引数が2つになる public static async UniTask InvokeWithErrorAsync(this X self, Func> connector) where X : IService { while (true) { try { return await connector(self); } // AggregateException から RpcException を抜き取る catch (AggregateException e) when (e.InnerException is RpcException rpc) { // 次のスライドで... } } }

Slide 39

Slide 39 text

リトライ - 実装方法 39 catch (AggregateException e) when (e.InnerException is RpcException rpc) { // ここの条件は要件次第 switch (rpc.StatusCode) { case StatusCode.Unavailable: if (await リトライかタイトルに戻るかを聞く() == "リトライ") continue; SceneManager.LoadSceneAsync("Title"); // OperationCanceledException を投げて処理を打ち切ったことを通知 throw new OperationCanceledException("タイトルに戻りました", rpc); } // その他の例外はそのまま投げる throw; }

Slide 40

Slide 40 text

まとめ 40

Slide 41

Slide 41 text

まとめ ● サーバー ○ MagicOnionFilter と gRPC Interceptor のうち 要件に合う方を選んで使う ● クライアント ○ gRPC Interceptor を使う ■ 複数挟むと例外が扱いづらくなるので1クラスで ○ gRPC Interceptor でできないことは泥臭く実装 41