MagicOnionでの共通処理の挟み方

9889f5d648eed7a85601688f8e3d51c2?s=47 pasta
June 04, 2019

 MagicOnionでの共通処理の挟み方

9889f5d648eed7a85601688f8e3d51c2?s=128

pasta

June 04, 2019
Tweet

Transcript

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

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

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

  4. やりたいこと • サーバー/クライアント ◦ 共通リクエスト/レスポンスの送受信 ◦ 暗号化 • クライアント ◦

    共通エラーハンドリング ◦ リトライ ◦ デバッグ出力 などなど…… 4
  5. サーバーサイド 5

  6. サーバーサイド 2通りのやりかた • MagicOnionFilter ◦ MagicOnionに用意された機能 • gRPC Interceptor ◦

    gRPC本来の機能 どちらもRPCの前後に処理を挟むもの 6
  7. MagicOnionFilter - 実装方法 MagicOnionFilterAttributeを継承する public class SampleFilterAttribute : MagicOnionFilterAttribute {

    public override async ValueTask Invoke(ServiceContext context) { try { /* 前処理 */ await Next(context); /* 後処理 */ } catch { /* エラー処理 */ throw; } finally { /* on finally */ } } } 7
  8. MagicOnionFilter - 適用方法 ①特定のクラス全体・または1つのメソッドに適用する場合 クラスまたはメソッドに属性として付与する // クラスに付けるとクラス全体に適用される [SampleFilter] public class

    SampleService : ServiceBase<ISampleService>, ISampleService { // メソッドに付けるとメソッドだけに適用される [SampleFilter] public async UnaryResult<Nil> HogeAsync() { } } 8
  9. MagicOnionFilter - 適用方法 ②全ての通信に適用する場合 MagicOnionOptions で指定する var options = new

    MagicOnionOptions { GlobalFilters = new[] { new SampleFilterAttribute() }, }); var service = MagicOnionEngine.BuildServerServiceDefinition(options); 9
  10. MagicOnionFilter - 概要図 10

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

    して実 装する 今回は UnaryServerHandler のみ説明 11
  12. gRPC Interceptor - 実装方法 public class SampleInterceptor : Interceptor {

    // ※MagicOnionを使っている限り TRequest, TResponse は byte[] 固定 public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, // 生リクエストデータ ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation ) { /* 前処理 */ var response = await continuation(request, context); /* 後処理 */ return response; } } 12
  13. gRPC Interceptor - 適用方法 MagicOnionServiceDefinition.ServerServiceDefinition から Interceptor を刺した ServerServiceDefinition を生成する

    var service = MagicOnionEngine.BuildServerServiceDefinition(); var serverService = service.ServerServiceDefinition.Intercept( new SampleInterceptor() ); 13
  14. 比較 MagicOnion Filter gRPC Interceptor 部分適用 ◯ ✕ メソッド本体との値のやり取り ◯

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

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

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

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

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

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

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

    ✕ リクエストヘッダの読みとり レスポンスヘッダの書き換え ◯ ◯ リクエストボディの書き換え ✕ ◯ レスポンスボディの書き換え △ ◯ クライアントと共通化 ✕ ◯ 21 要件に合う方を選んで使うのが重要
  22. サンプル - 共通リクエスト/レスポンスの送受信 • 要件 ◦ メソッド本体との値のやり取り ◦ リクエストヘッダの読みとり ◦

    レスポンスヘッダの書き換え MagicOnionFilter 22
  23. 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<CommonRequest>(header.ValueBytes); /* 共通リクエストに対する処理 */ context.Items[Key] = commonRequest; // メソッドで使うために詰めておく await Next(context); var commonResponse = new CommonResponse { /* 共通レスポンス作成 */ }; await context.CallContext.WriteResponseHeadersAsync(new Metadata { { Key, LZ4MessagePackSerializer.Serialize(commonResponse) } }); } } 23
  24. サンプル - 暗号化 • 要件 ◦ リクエストボディの書き換え ◦ レスポンスボディの書き換え ◦

    クライアントと共通化 gRPC Interceptor 24
  25. public class CryptoInterceptor : Interceptor { public const string Key

    = "暗号鍵"; public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> 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
  26. クライアントサイド 26

  27. クライアントサイド - 実装方法 • 基本的に gRPC Interceptor 一択 • MagicOnionClient.Create

    に Interceptor を挟んだ CallInvoker を渡す var channel = new Channel("localhost", 12345, ChannelCredentials.Insecure); var invoker = channel.Intercept(new HogeInterceptor()); var client = MagicOnionClient.Create<IHogeService>(invoker); 27
  28. クライアントサイド - 実装方法 28 public class SampleInterceptor : Interceptor {

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

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

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

    new AsyncUnaryCall<TResponse>( call.ResponseAsync.ContinueWith(async res => { /* 後処理 */ return res.Result; }).Unwrap(), call.ResponseHeadersAsync, call.GetStatus, call.GetTrailers, call.Dispose ); • 後処理で await したい場合
  32. クライアントサイド - 実装方法 32 • 前処理で await したい場合 ◦ 不明……

  33. エラーハンドリング 33

  34. エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ gRPC 的には RpcException が発生するはず

    ◦ 実際は AggregateException が発生 ▪ Task 内部で発生した例外は AggregateException に 集約される ▪ 元の例外は .InnerExceptions で取得 34
  35. エラーハンドリング - 落とし穴 • 例外が発生した場合 ◦ Interceptor を重ねるとその数だけ AggregateException でラップされる

    ▪ めちゃくちゃ取り出しづらい ▪ 本来は機能単位で Interceptor を分けるべきだが、例 外が扱いづらくなるため1クラスにまとめたほうが良い かもしれない • 結局サーバーと共通化できない 35
  36. エラーハンドリング - リトライ • 一定の例外だったらリトライしたい • Interceptor でリトライをする方法はなさそう(※独自調べ) 拡張メソッドを作って、必ずそこ経由で呼ぶようにするしかなさそう 36

  37. エラーハンドリング - リトライ 37 // InvokeWithErrorAsync 経由で呼ぶ MagicOnionClient.Create<IHogeService>().InvokeWithErrorAsync(x => x.HogeAsync());

    // 通常はこう // MagicOnionClient.Create<IHogeService>().HogeAsync(); • イメージ (めんどくさい……)
  38. リトライ - 実装方法 38 // IService からメソッドチェーンしたいので型引数が2つになる public static async

    UniTask<T> InvokeWithErrorAsync<T, X>(this X self, Func<X, UnaryResult<T>> connector) where X : IService<X> { while (true) { try { return await connector(self); } // AggregateException から RpcException を抜き取る catch (AggregateException e) when (e.InnerException is RpcException rpc) { // 次のスライドで... } } }
  39. リトライ - 実装方法 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; }
  40. まとめ 40

  41. まとめ • サーバー ◦ MagicOnionFilter と gRPC Interceptor のうち 要件に合う方を選んで使う

    • クライアント ◦ gRPC Interceptor を使う ▪ 複数挟むと例外が扱いづらくなるので1クラスで ◦ gRPC Interceptor でできないことは泥臭く実装 41